craft-native 0.0.140 → 0.0.141
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/templates/CraftNative.kt.template +137 -0
- package/dist/android/templates/MainActivityNative.kt.template +822 -87
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.js +1 -0
- package/dist/ios/templates/CraftApp.swift +14 -482
- package/dist/ios/templates/CraftHealth.swift +551 -0
- package/dist/ios/templates/CraftNativeActions.swift +11 -0
- package/dist/ios/templates/CraftNativeScreen.swift +753 -52
- package/package.json +1 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
+
import AVFoundation
|
|
3
|
+
import AVKit
|
|
4
|
+
import Darwin
|
|
2
5
|
import JavaScriptCore
|
|
3
6
|
import OSLog
|
|
4
7
|
import SwiftUI
|
|
@@ -18,6 +21,477 @@ private func craftNativeGridColumnCount(_ value: Any?) -> Int {
|
|
|
18
21
|
return max(1, tracks.count)
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
/// Optional bridge to the shared Zig flex rule. Native hosts retain their
|
|
25
|
+
/// local implementation when no Craft runtime is linked (the normal SDK
|
|
26
|
+
/// fallback), while runtime-backed apps get one cross-platform calculation.
|
|
27
|
+
private enum CraftNativeFlexBridge {
|
|
28
|
+
private struct Item {
|
|
29
|
+
var basis: Float
|
|
30
|
+
var grow: Float
|
|
31
|
+
var shrink: Float
|
|
32
|
+
var min: Float
|
|
33
|
+
var max: Float
|
|
34
|
+
var marginStart: Float
|
|
35
|
+
var marginEnd: Float
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private typealias Function = @convention(c) (
|
|
39
|
+
Float, Float, UInt32, UnsafeRawPointer, Int, UnsafeMutableRawPointer, UnsafeMutableRawPointer
|
|
40
|
+
) -> Int32
|
|
41
|
+
|
|
42
|
+
private static let function: Function? = {
|
|
43
|
+
guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "craft_native_flex_row_justified") else { return nil }
|
|
44
|
+
return unsafeBitCast(symbol, to: Function.self)
|
|
45
|
+
}()
|
|
46
|
+
|
|
47
|
+
static func widths(available: CGFloat, gap: CGFloat, justify: UInt32, items: [(basis: CGFloat, grow: CGFloat, shrink: CGFloat, min: CGFloat?, max: CGFloat?, leading: CGFloat, trailing: CGFloat)]) -> [(width: CGFloat, position: CGFloat)]? {
|
|
48
|
+
guard let function, !items.isEmpty else { return nil }
|
|
49
|
+
var input = items.map { item in
|
|
50
|
+
Item(
|
|
51
|
+
basis: Float(item.basis), grow: Float(item.grow), shrink: Float(item.shrink),
|
|
52
|
+
min: item.min.map(Float.init) ?? .nan, max: item.max.map(Float.init) ?? .nan,
|
|
53
|
+
marginStart: Float(item.leading), marginEnd: Float(item.trailing)
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
var output = [Float](repeating: 0, count: input.count)
|
|
57
|
+
var positions = [Float](repeating: 0, count: input.count)
|
|
58
|
+
let status = input.withUnsafeBufferPointer { inputBuffer in
|
|
59
|
+
output.withUnsafeMutableBufferPointer { outputBuffer in
|
|
60
|
+
positions.withUnsafeMutableBufferPointer { positionBuffer in
|
|
61
|
+
function(Float(available), Float(gap), justify, UnsafeRawPointer(inputBuffer.baseAddress!), input.count, UnsafeMutableRawPointer(outputBuffer.baseAddress!), UnsafeMutableRawPointer(positionBuffer.baseAddress!))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
guard status == 0, output.allSatisfy({ $0.isFinite }), positions.allSatisfy({ $0.isFinite }) else { return nil }
|
|
66
|
+
return zip(output, positions).map { (width: CGFloat($0), position: CGFloat($1)) }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private func craftNativeJustifyCode(_ value: String) -> UInt32 {
|
|
71
|
+
switch value {
|
|
72
|
+
case "center": return 1
|
|
73
|
+
case "flex-end": return 2
|
|
74
|
+
case "space-between": return 3
|
|
75
|
+
case "space-around": return 4
|
|
76
|
+
case "space-evenly": return 5
|
|
77
|
+
default: return 0
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private enum CraftNativeFlexLineBridge {
|
|
82
|
+
private struct Item {
|
|
83
|
+
var basis: Float
|
|
84
|
+
var marginStart: Float
|
|
85
|
+
var marginEnd: Float
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private typealias Function = @convention(c) (
|
|
89
|
+
Float, Float, UInt32, UnsafeRawPointer, Int, UnsafeMutableRawPointer
|
|
90
|
+
) -> Int32
|
|
91
|
+
|
|
92
|
+
private static let function: Function? = {
|
|
93
|
+
guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "craft_native_flex_line_breaks") else { return nil }
|
|
94
|
+
return unsafeBitCast(symbol, to: Function.self)
|
|
95
|
+
}()
|
|
96
|
+
|
|
97
|
+
static func lines(available: CGFloat, gap: CGFloat, wrap: Bool, items: [(basis: CGFloat, leading: CGFloat, trailing: CGFloat)]) -> [Int]? {
|
|
98
|
+
guard let function, !items.isEmpty else { return nil }
|
|
99
|
+
var input = items.map { Item(basis: Float($0.basis), marginStart: Float($0.leading), marginEnd: Float($0.trailing)) }
|
|
100
|
+
var output = [Int32](repeating: 0, count: input.count)
|
|
101
|
+
let status = input.withUnsafeBufferPointer { inputBuffer in
|
|
102
|
+
output.withUnsafeMutableBufferPointer { outputBuffer in
|
|
103
|
+
function(Float(available), Float(gap), wrap ? 1 : 0, UnsafeRawPointer(inputBuffer.baseAddress!), input.count, UnsafeMutableRawPointer(outputBuffer.baseAddress!))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
guard status > 0 else { return nil }
|
|
107
|
+
return output.map(Int.init)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private enum CraftNativeFlexLineDistributionBridge {
|
|
112
|
+
private typealias Function = @convention(c) (
|
|
113
|
+
Float, Float, UInt32, UnsafeRawPointer, Int, UnsafeMutableRawPointer, UnsafeMutableRawPointer
|
|
114
|
+
) -> Int32
|
|
115
|
+
|
|
116
|
+
private static let function: Function? = {
|
|
117
|
+
guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "craft_native_flex_line_distribution") else { return nil }
|
|
118
|
+
return unsafeBitCast(symbol, to: Function.self)
|
|
119
|
+
}()
|
|
120
|
+
|
|
121
|
+
static func distribute(available: CGFloat, gap: CGFloat, alignment: UInt32, lineCrosses: [CGFloat]) -> (offsets: [CGFloat], sizes: [CGFloat])? {
|
|
122
|
+
guard let function, !lineCrosses.isEmpty else { return nil }
|
|
123
|
+
var input = lineCrosses.map(Float.init)
|
|
124
|
+
var offsets = [Float](repeating: 0, count: input.count)
|
|
125
|
+
var sizes = [Float](repeating: 0, count: input.count)
|
|
126
|
+
let status = input.withUnsafeBufferPointer { inputBuffer in
|
|
127
|
+
offsets.withUnsafeMutableBufferPointer { offsetBuffer in
|
|
128
|
+
sizes.withUnsafeMutableBufferPointer { sizeBuffer in
|
|
129
|
+
function(Float(available), Float(gap), alignment, UnsafeRawPointer(inputBuffer.baseAddress!), input.count, UnsafeMutableRawPointer(offsetBuffer.baseAddress!), UnsafeMutableRawPointer(sizeBuffer.baseAddress!))
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
guard status == 0, offsets.allSatisfy({ $0.isFinite }), sizes.allSatisfy({ $0.isFinite }) else { return nil }
|
|
134
|
+
return (offsets.map { CGFloat($0) }, sizes.map { CGFloat($0) })
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Optional bridge to the shared cross-axis alignment rule. The local rule is
|
|
139
|
+
/// retained for runtimes built before this symbol was exported.
|
|
140
|
+
private enum CraftNativeCrossAxisBridge {
|
|
141
|
+
private typealias Function = @convention(c) (Float, Float, Float, Float, UInt32) -> Float
|
|
142
|
+
|
|
143
|
+
private static let function: Function? = {
|
|
144
|
+
guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "craft_native_cross_axis_position") else { return nil }
|
|
145
|
+
return unsafeBitCast(symbol, to: Function.self)
|
|
146
|
+
}()
|
|
147
|
+
|
|
148
|
+
static func position(available: CGFloat, size: CGFloat, leading: CGFloat, trailing: CGFloat, alignment: UInt32) -> CGFloat? {
|
|
149
|
+
guard let function else { return nil }
|
|
150
|
+
let result = function(Float(available), Float(size), Float(leading), Float(trailing), alignment)
|
|
151
|
+
return result.isFinite ? CGFloat(result) : nil
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private enum CraftNativeGridBridge {
|
|
156
|
+
private struct Item {
|
|
157
|
+
var intrinsicWidth: Float
|
|
158
|
+
var intrinsicHeight: Float
|
|
159
|
+
var explicitWidth: Float
|
|
160
|
+
var explicitHeight: Float
|
|
161
|
+
var minWidth: Float
|
|
162
|
+
var maxWidth: Float
|
|
163
|
+
var minHeight: Float
|
|
164
|
+
var maxHeight: Float
|
|
165
|
+
var marginLeft: Float
|
|
166
|
+
var marginRight: Float
|
|
167
|
+
var marginTop: Float
|
|
168
|
+
var marginBottom: Float
|
|
169
|
+
var stretch: UInt32
|
|
170
|
+
var alignment: UInt32
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private struct Frame {
|
|
174
|
+
var x: Float
|
|
175
|
+
var y: Float
|
|
176
|
+
var width: Float
|
|
177
|
+
var height: Float
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private typealias Function = @convention(c) (
|
|
181
|
+
Float, Float, UInt32, Float, Float, UnsafeRawPointer, Int, UnsafeMutableRawPointer
|
|
182
|
+
) -> Int32
|
|
183
|
+
|
|
184
|
+
private static let function: Function? = {
|
|
185
|
+
guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "craft_native_grid") else { return nil }
|
|
186
|
+
return unsafeBitCast(symbol, to: Function.self)
|
|
187
|
+
}()
|
|
188
|
+
|
|
189
|
+
static func frames(
|
|
190
|
+
availableWidth: CGFloat,
|
|
191
|
+
availableHeight: CGFloat,
|
|
192
|
+
columns: Int,
|
|
193
|
+
columnGap: CGFloat,
|
|
194
|
+
rowGap: CGFloat,
|
|
195
|
+
items: [(intrinsicWidth: CGFloat, intrinsicHeight: CGFloat, explicitWidth: CGFloat?, explicitHeight: CGFloat?, minWidth: CGFloat?, maxWidth: CGFloat?, minHeight: CGFloat?, maxHeight: CGFloat?, marginLeft: CGFloat, marginRight: CGFloat, marginTop: CGFloat, marginBottom: CGFloat, stretch: Bool, alignment: UInt32)]
|
|
196
|
+
) -> [(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat)]? {
|
|
197
|
+
guard let function, !items.isEmpty else { return nil }
|
|
198
|
+
var input = items.map { item in
|
|
199
|
+
Item(
|
|
200
|
+
intrinsicWidth: Float(item.intrinsicWidth), intrinsicHeight: Float(item.intrinsicHeight),
|
|
201
|
+
explicitWidth: item.explicitWidth.map(Float.init) ?? .nan, explicitHeight: item.explicitHeight.map(Float.init) ?? .nan,
|
|
202
|
+
minWidth: item.minWidth.map(Float.init) ?? .nan, maxWidth: item.maxWidth.map(Float.init) ?? .nan,
|
|
203
|
+
minHeight: item.minHeight.map(Float.init) ?? .nan, maxHeight: item.maxHeight.map(Float.init) ?? .nan,
|
|
204
|
+
marginLeft: Float(item.marginLeft), marginRight: Float(item.marginRight),
|
|
205
|
+
marginTop: Float(item.marginTop), marginBottom: Float(item.marginBottom),
|
|
206
|
+
stretch: item.stretch ? 1 : 0, alignment: item.alignment
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
var output = [Frame](repeating: Frame(x: 0, y: 0, width: 0, height: 0), count: input.count)
|
|
210
|
+
let status = input.withUnsafeBufferPointer { inputBuffer in
|
|
211
|
+
output.withUnsafeMutableBufferPointer { outputBuffer in
|
|
212
|
+
function(Float(availableWidth), Float(availableHeight), UInt32(max(1, columns)), Float(columnGap), Float(rowGap), UnsafeRawPointer(inputBuffer.baseAddress!), input.count, UnsafeMutableRawPointer(outputBuffer.baseAddress!))
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
guard status == 0, output.allSatisfy({ frame in
|
|
216
|
+
frame.x.isFinite && frame.y.isFinite && frame.width.isFinite && frame.height.isFinite
|
|
217
|
+
}) else { return nil }
|
|
218
|
+
return output.map { (CGFloat($0.x), CGFloat($0.y), CGFloat($0.width), CGFloat($0.height)) }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private final class CraftNativeVideoView: UIView {
|
|
223
|
+
private let playerController = AVPlayerViewController()
|
|
224
|
+
private var player: AVPlayer?
|
|
225
|
+
private var sourceURI = ""
|
|
226
|
+
private var endObserver: NSObjectProtocol?
|
|
227
|
+
private var loops = false
|
|
228
|
+
|
|
229
|
+
init(host: UIViewController) {
|
|
230
|
+
super.init(frame: .zero)
|
|
231
|
+
clipsToBounds = true
|
|
232
|
+
host.addChild(playerController)
|
|
233
|
+
addSubview(playerController.view)
|
|
234
|
+
playerController.didMove(toParent: host)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
required init?(coder: NSCoder) {
|
|
238
|
+
super.init(coder: coder)
|
|
239
|
+
clipsToBounds = true
|
|
240
|
+
addSubview(playerController.view)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
override func layoutSubviews() {
|
|
244
|
+
super.layoutSubviews()
|
|
245
|
+
playerController.view.frame = bounds
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
func configure(source: Any?, autoplay: Bool, paused: Bool, loop: Bool, muted: Bool, controls: Bool, resizeMode: String?) {
|
|
249
|
+
let uri = (source as? String) ?? (source as? [String: Any])?["uri"] as? String ?? ""
|
|
250
|
+
if uri != sourceURI {
|
|
251
|
+
if let observer = endObserver { NotificationCenter.default.removeObserver(observer) }
|
|
252
|
+
endObserver = nil
|
|
253
|
+
player?.pause()
|
|
254
|
+
player = nil
|
|
255
|
+
sourceURI = uri
|
|
256
|
+
loops = false
|
|
257
|
+
guard let url = URL(string: uri), !uri.isEmpty, url.scheme != nil else {
|
|
258
|
+
playerController.player = nil
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
let next = AVPlayer(url: url)
|
|
262
|
+
player = next
|
|
263
|
+
playerController.player = next
|
|
264
|
+
}
|
|
265
|
+
guard let player else { return }
|
|
266
|
+
player.isMuted = muted
|
|
267
|
+
playerController.showsPlaybackControls = controls
|
|
268
|
+
playerController.videoGravity = resizeMode == "cover" ? .resizeAspectFill
|
|
269
|
+
: resizeMode == "stretch" ? .resize
|
|
270
|
+
: .resizeAspect
|
|
271
|
+
if loops != loop {
|
|
272
|
+
if let observer = endObserver { NotificationCenter.default.removeObserver(observer) }
|
|
273
|
+
endObserver = nil
|
|
274
|
+
loops = loop
|
|
275
|
+
if loop, let item = player.currentItem {
|
|
276
|
+
endObserver = NotificationCenter.default.addObserver(
|
|
277
|
+
forName: .AVPlayerItemDidPlayToEndTime,
|
|
278
|
+
object: item,
|
|
279
|
+
queue: .main
|
|
280
|
+
) { [weak player] _ in
|
|
281
|
+
player?.seek(to: .zero) { _ in player?.play() }
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if paused || !autoplay { player.pause() } else { player.play() }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
func stop() {
|
|
289
|
+
player?.pause()
|
|
290
|
+
if let observer = endObserver { NotificationCenter.default.removeObserver(observer) }
|
|
291
|
+
endObserver = nil
|
|
292
|
+
player = nil
|
|
293
|
+
playerController.player = nil
|
|
294
|
+
sourceURI = ""
|
|
295
|
+
if playerController.parent != nil {
|
|
296
|
+
playerController.willMove(toParent: nil)
|
|
297
|
+
playerController.view.removeFromSuperview()
|
|
298
|
+
playerController.removeFromParent()
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
deinit { stop() }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/// The small Web Audio subset used by native STX screens: oscillator and gain
|
|
306
|
+
/// nodes connected to the context destination. JavaScriptCore has no Web Audio
|
|
307
|
+
/// implementation, so the screen installs a compatible surface and routes the
|
|
308
|
+
/// graph to one AVAudioEngine source node per oscillator.
|
|
309
|
+
private final class CraftNativeAudio {
|
|
310
|
+
private struct ParameterEvent {
|
|
311
|
+
let time: Double
|
|
312
|
+
let value: Double
|
|
313
|
+
let ramp: Bool
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private final class Parameter {
|
|
317
|
+
private let lock = NSLock()
|
|
318
|
+
private var current: Double
|
|
319
|
+
private var events: [ParameterEvent] = []
|
|
320
|
+
|
|
321
|
+
init(_ value: Double) {
|
|
322
|
+
current = value
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
func set(_ value: Double) {
|
|
326
|
+
lock.lock(); defer { lock.unlock() }
|
|
327
|
+
current = value
|
|
328
|
+
events.removeAll()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
func schedule(_ value: Double, at time: Double, ramp: Bool) {
|
|
332
|
+
lock.lock(); defer { lock.unlock() }
|
|
333
|
+
events.append(ParameterEvent(time: max(0, time), value: value, ramp: ramp))
|
|
334
|
+
events.sort { $0.time < $1.time }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
func value(at time: Double) -> Double {
|
|
338
|
+
lock.lock(); defer { lock.unlock() }
|
|
339
|
+
var previousTime = 0.0
|
|
340
|
+
var previousValue = current
|
|
341
|
+
for event in events {
|
|
342
|
+
if time < event.time {
|
|
343
|
+
guard event.ramp, event.time > previousTime else { return previousValue }
|
|
344
|
+
let fraction = (time - previousTime) / (event.time - previousTime)
|
|
345
|
+
return previousValue + (event.value - previousValue) * fraction
|
|
346
|
+
}
|
|
347
|
+
previousTime = event.time
|
|
348
|
+
previousValue = event.value
|
|
349
|
+
}
|
|
350
|
+
return previousValue
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private final class OscillatorState {
|
|
355
|
+
let lock = NSLock()
|
|
356
|
+
let frequency = Parameter(440)
|
|
357
|
+
var gain = Parameter(1)
|
|
358
|
+
var phase = 0.0
|
|
359
|
+
var active = false
|
|
360
|
+
let contextStart: TimeInterval
|
|
361
|
+
|
|
362
|
+
init(contextStart: TimeInterval) {
|
|
363
|
+
self.contextStart = contextStart
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private let engine = AVAudioEngine()
|
|
368
|
+
private var format: AVAudioFormat?
|
|
369
|
+
private var sources: [String: (state: OscillatorState, node: AVAudioSourceNode)] = [:]
|
|
370
|
+
private var gains: [String: Parameter] = [:]
|
|
371
|
+
private var contextStarts: [String: TimeInterval] = [:]
|
|
372
|
+
private var closed = false
|
|
373
|
+
|
|
374
|
+
func handle(_ payload: [String: Any]) {
|
|
375
|
+
guard !closed, let action = payload["action"] as? String else { return }
|
|
376
|
+
switch action {
|
|
377
|
+
case "createContext":
|
|
378
|
+
closed = false
|
|
379
|
+
if let contextID = payload["contextId"] as? String {
|
|
380
|
+
contextStarts[contextID] = Date().timeIntervalSince1970
|
|
381
|
+
}
|
|
382
|
+
case "createOscillator":
|
|
383
|
+
guard let id = payload["nodeId"] as? String else { return }
|
|
384
|
+
let contextStart = (payload["contextId"] as? String).flatMap { contextStarts[$0] } ?? Date().timeIntervalSince1970
|
|
385
|
+
createOscillator(id: id, frequency: number(payload["frequency"]) ?? 440, contextStart: contextStart)
|
|
386
|
+
case "createGain":
|
|
387
|
+
guard let id = payload["nodeId"] as? String else { return }
|
|
388
|
+
gains[id] = Parameter(number(payload["gain"]) ?? 1)
|
|
389
|
+
case "connect":
|
|
390
|
+
guard let sourceID = payload["sourceId"] as? String else { return }
|
|
391
|
+
let destinationID = payload["destinationId"] as? String
|
|
392
|
+
if var source = sources[sourceID] {
|
|
393
|
+
source.state.lock.lock()
|
|
394
|
+
source.state.gain = destinationID.flatMap { gains[$0] } ?? Parameter(1)
|
|
395
|
+
source.state.lock.unlock()
|
|
396
|
+
sources[sourceID] = source
|
|
397
|
+
}
|
|
398
|
+
case "frequency":
|
|
399
|
+
guard let id = payload["nodeId"] as? String, let value = number(payload["value"]), let source = sources[id] else { return }
|
|
400
|
+
source.state.frequency.set(max(0, value))
|
|
401
|
+
case "frequencyAtTime", "frequencyRampToTime":
|
|
402
|
+
guard let id = payload["nodeId"] as? String, let value = number(payload["value"]), let time = number(payload["time"]), let source = sources[id] else { return }
|
|
403
|
+
source.state.frequency.schedule(max(0, value), at: time, ramp: action == "frequencyRampToTime")
|
|
404
|
+
case "gain":
|
|
405
|
+
guard let id = payload["nodeId"] as? String, let value = number(payload["value"]) else { return }
|
|
406
|
+
gains[id]?.set(max(0, value))
|
|
407
|
+
case "gainAtTime", "gainRampToTime":
|
|
408
|
+
guard let id = payload["nodeId"] as? String, let value = number(payload["value"]), let time = number(payload["time"]) else { return }
|
|
409
|
+
gains[id]?.schedule(max(0, value), at: time, ramp: action == "gainRampToTime")
|
|
410
|
+
case "start", "stop":
|
|
411
|
+
guard let id = payload["nodeId"] as? String, let source = sources[id] else { return }
|
|
412
|
+
if action == "start" { start(source.state, after: max(0, number(payload["delay"]) ?? 0)) }
|
|
413
|
+
else { stop(source.state, after: max(0, number(payload["delay"]) ?? 0)) }
|
|
414
|
+
case "close":
|
|
415
|
+
close()
|
|
416
|
+
default:
|
|
417
|
+
break
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
func close() {
|
|
422
|
+
guard !closed else { return }
|
|
423
|
+
closed = true
|
|
424
|
+
sources.values.forEach { source in
|
|
425
|
+
source.state.lock.lock(); source.state.active = false; source.state.lock.unlock()
|
|
426
|
+
engine.detach(source.node)
|
|
427
|
+
}
|
|
428
|
+
sources.removeAll()
|
|
429
|
+
gains.removeAll()
|
|
430
|
+
contextStarts.removeAll()
|
|
431
|
+
engine.stop()
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private func createOscillator(id: String, frequency: Double, contextStart: TimeInterval) {
|
|
435
|
+
guard sources[id] == nil else { return }
|
|
436
|
+
guard let format = audioFormat() else { return }
|
|
437
|
+
let state = OscillatorState(contextStart: contextStart)
|
|
438
|
+
state.frequency.set(max(0, frequency))
|
|
439
|
+
let node = AVAudioSourceNode(format: format) { _, _, frameCount, audioBufferList in
|
|
440
|
+
let buffers = UnsafeMutableAudioBufferListPointer(audioBufferList)
|
|
441
|
+
state.lock.lock()
|
|
442
|
+
let active = state.active
|
|
443
|
+
let frequency = state.frequency
|
|
444
|
+
let gain = state.gain
|
|
445
|
+
var phase = state.phase
|
|
446
|
+
state.lock.unlock()
|
|
447
|
+
let startTime = max(0, Date().timeIntervalSince1970 - state.contextStart)
|
|
448
|
+
for buffer in buffers {
|
|
449
|
+
guard let data = buffer.mData else { continue }
|
|
450
|
+
let samples = data.assumingMemoryBound(to: Float.self)
|
|
451
|
+
for frame in 0..<Int(frameCount) {
|
|
452
|
+
let frameTime = startTime + Double(frame) / format.sampleRate
|
|
453
|
+
let frameFrequency = frequency.value(at: frameTime)
|
|
454
|
+
let frameGain = gain.value(at: frameTime)
|
|
455
|
+
samples[frame] = active ? Float(sin(phase) * frameGain) : 0
|
|
456
|
+
phase += 2 * Double.pi * frameFrequency / format.sampleRate
|
|
457
|
+
if phase >= 2 * Double.pi { phase -= 2 * Double.pi }
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
state.lock.lock(); state.phase = phase; state.lock.unlock()
|
|
461
|
+
return noErr
|
|
462
|
+
}
|
|
463
|
+
engine.attach(node)
|
|
464
|
+
engine.connect(node, to: engine.mainMixerNode, format: format)
|
|
465
|
+
sources[id] = (state, node)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private func audioFormat() -> AVAudioFormat? {
|
|
469
|
+
if let format { return format }
|
|
470
|
+
let created = AVAudioFormat(standardFormatWithSampleRate: 44_100, channels: 2)
|
|
471
|
+
format = created
|
|
472
|
+
return created
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private func start(_ state: OscillatorState, after delay: Double) {
|
|
476
|
+
if !engine.isRunning {
|
|
477
|
+
try? engine.start()
|
|
478
|
+
}
|
|
479
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
|
480
|
+
state.lock.lock(); state.active = true; state.lock.unlock()
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private func stop(_ state: OscillatorState, after delay: Double) {
|
|
485
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
|
486
|
+
state.lock.lock(); state.active = false; state.lock.unlock()
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private func number(_ value: Any?) -> Double? {
|
|
491
|
+
(value as? NSNumber).map { $0.doubleValue }
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
21
495
|
/// A WebView-free host for the first stx native vertical slice. The bundled
|
|
22
496
|
/// JavaScript sends whole, compiled view trees to UIKit and receives control
|
|
23
497
|
/// events and Craft API replies through JavaScriptCore.
|
|
@@ -249,6 +723,7 @@ private final class CraftNativeFlowView: UIStackView {
|
|
|
249
723
|
var grid = false { didSet { if grid != oldValue { invalidateMeasurements() } } }
|
|
250
724
|
var gridColumns = 1 { didSet { if gridColumns != oldValue { invalidateMeasurements() } } }
|
|
251
725
|
var gridAutoRows: CGFloat? { didSet { if gridAutoRows != oldValue { invalidateMeasurements() } } }
|
|
726
|
+
var reverse = false { didSet { if reverse != oldValue { invalidateMeasurements() } } }
|
|
252
727
|
var padding = UIEdgeInsets.zero {
|
|
253
728
|
didSet {
|
|
254
729
|
layoutMargins = padding
|
|
@@ -572,44 +1047,73 @@ private final class CraftNativeFlowView: UIStackView {
|
|
|
572
1047
|
// Lines: one, or as many as wrapping needs.
|
|
573
1048
|
var lines: [Range<Int>] = []
|
|
574
1049
|
let lineLimit = definiteMain ?? roomMain
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1050
|
+
if let lineIds = CraftNativeFlexLineBridge.lines(
|
|
1051
|
+
available: lineLimit,
|
|
1052
|
+
gap: mainGap,
|
|
1053
|
+
wrap: wrap,
|
|
1054
|
+
items: items.map { ($0.basis, $0.mainLeading, $0.mainTrailing) }
|
|
1055
|
+
) {
|
|
1056
|
+
var start = 0
|
|
1057
|
+
for index in 1..<items.count where lineIds[index] != lineIds[index - 1] {
|
|
581
1058
|
lines.append(start..<index)
|
|
582
1059
|
start = index
|
|
583
|
-
used = size
|
|
584
|
-
} else {
|
|
585
|
-
used = next
|
|
586
1060
|
}
|
|
1061
|
+
lines.append(start..<items.count)
|
|
1062
|
+
} else {
|
|
1063
|
+
var start = 0
|
|
1064
|
+
var used: CGFloat = 0
|
|
1065
|
+
for index in items.indices {
|
|
1066
|
+
let size = items[index].basis + items[index].mainMargins
|
|
1067
|
+
let next = index == start ? size : used + mainGap + size
|
|
1068
|
+
if wrap, index > start, next > lineLimit {
|
|
1069
|
+
lines.append(start..<index)
|
|
1070
|
+
start = index
|
|
1071
|
+
used = size
|
|
1072
|
+
} else {
|
|
1073
|
+
used = next
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
if start < items.count { lines.append(start..<items.count) }
|
|
587
1077
|
}
|
|
588
|
-
if start < items.count { lines.append(start..<items.count) }
|
|
589
1078
|
|
|
590
1079
|
var lineMains: [CGFloat] = []
|
|
591
1080
|
var lineCrosses: [CGFloat] = []
|
|
1081
|
+
var sharedLines: [[(width: CGFloat, position: CGFloat)]?] = []
|
|
592
1082
|
for line in lines {
|
|
593
1083
|
let gaps = CGFloat(max(0, line.count - 1)) * mainGap
|
|
594
1084
|
let hypothetical = line.reduce(0) { $0 + items[$1].basis + items[$1].mainMargins } + gaps
|
|
595
1085
|
// A row sized by its content is as wide as that content, up to the
|
|
596
1086
|
// room it has; a column sized by its content is as tall as it is.
|
|
597
1087
|
let lineMain = definiteMain ?? (horizontal ? min(hypothetical, roomMain) : hypothetical)
|
|
598
|
-
let
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
} else if free < 0 {
|
|
606
|
-
let total = line.reduce(0) { $0 + items[$1].shrink * items[$1].basis }
|
|
607
|
-
for index in line {
|
|
608
|
-
let share = total > 0 ? -free * items[index].shrink * items[index].basis / total : 0
|
|
609
|
-
items[index].main = clamp(items[index].basis - share, items[index].minMain, items[index].maxMain)
|
|
1088
|
+
let sharedWidths = CraftNativeFlexBridge.widths(
|
|
1089
|
+
available: lineMain,
|
|
1090
|
+
gap: mainGap,
|
|
1091
|
+
justify: craftNativeJustifyCode(justifyContent),
|
|
1092
|
+
items: line.map { index in
|
|
1093
|
+
let item = items[index]
|
|
1094
|
+
return (item.basis, item.grow, item.shrink, item.minMain, item.maxMain, item.mainLeading, item.mainTrailing)
|
|
610
1095
|
}
|
|
1096
|
+
)
|
|
1097
|
+
sharedLines.append(sharedWidths)
|
|
1098
|
+
if let sharedWidths {
|
|
1099
|
+
for (offset, index) in line.enumerated() { items[index].main = sharedWidths[offset].width }
|
|
611
1100
|
} else {
|
|
612
|
-
|
|
1101
|
+
let free = lineMain - hypothetical
|
|
1102
|
+
if free > 0 {
|
|
1103
|
+
let total = line.reduce(0) { $0 + items[$1].grow }
|
|
1104
|
+
for index in line {
|
|
1105
|
+
let share = total > 0 ? free * items[index].grow / total : 0
|
|
1106
|
+
items[index].main = clamp(items[index].basis + share, items[index].minMain, items[index].maxMain)
|
|
1107
|
+
}
|
|
1108
|
+
} else if free < 0 {
|
|
1109
|
+
let total = line.reduce(0) { $0 + items[$1].shrink * items[$1].basis }
|
|
1110
|
+
for index in line {
|
|
1111
|
+
let share = total > 0 ? -free * items[index].shrink * items[index].basis / total : 0
|
|
1112
|
+
items[index].main = clamp(items[index].basis - share, items[index].minMain, items[index].maxMain)
|
|
1113
|
+
}
|
|
1114
|
+
} else {
|
|
1115
|
+
for index in line { items[index].main = items[index].basis }
|
|
1116
|
+
}
|
|
613
1117
|
}
|
|
614
1118
|
|
|
615
1119
|
var lineCross: CGFloat = 0
|
|
@@ -646,19 +1150,50 @@ private final class CraftNativeFlowView: UIStackView {
|
|
|
646
1150
|
?? (lineCrosses.reduce(0, +) + CGFloat(max(0, lineCrosses.count - 1)) * crossGap)
|
|
647
1151
|
var crossLeading: CGFloat = 0
|
|
648
1152
|
var crossBetween = crossGap
|
|
1153
|
+
var sharedCrossOffsets: [CGFloat]? = nil
|
|
649
1154
|
if wrap, let definiteCross, lineCrosses.count > 1 {
|
|
650
|
-
let
|
|
651
|
-
|
|
652
|
-
case "
|
|
653
|
-
case "
|
|
654
|
-
case "space-
|
|
655
|
-
case "
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1155
|
+
let alignment: UInt32 = switch alignContent {
|
|
1156
|
+
case "center": 1
|
|
1157
|
+
case "flex-end": 2
|
|
1158
|
+
case "space-between": 3
|
|
1159
|
+
case "space-around": 4
|
|
1160
|
+
case "stretch": 5
|
|
1161
|
+
default: 0
|
|
1162
|
+
}
|
|
1163
|
+
if let distributed = CraftNativeFlexLineDistributionBridge.distribute(
|
|
1164
|
+
available: definiteCross,
|
|
1165
|
+
gap: crossGap,
|
|
1166
|
+
alignment: alignment,
|
|
1167
|
+
lineCrosses: lineCrosses
|
|
1168
|
+
) {
|
|
1169
|
+
sharedCrossOffsets = distributed.offsets
|
|
1170
|
+
lineCrosses = distributed.sizes
|
|
1171
|
+
for (lineIndex, line) in lines.enumerated() {
|
|
1172
|
+
for index in line where items[index].fixedCross == nil && items[index].align == "stretch" {
|
|
1173
|
+
let item = items[index]
|
|
1174
|
+
items[index].cross = clamp(lineCrosses[lineIndex] - item.crossMargins, item.minCross, item.maxCross)
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
} else {
|
|
1178
|
+
let free = max(0, definiteCross - (lineCrosses.reduce(0, +) + CGFloat(lineCrosses.count - 1) * crossGap))
|
|
1179
|
+
switch alignContent {
|
|
1180
|
+
case "center": crossLeading = free / 2
|
|
1181
|
+
case "flex-end": crossLeading = free
|
|
1182
|
+
case "space-between": crossBetween = free / CGFloat(lineCrosses.count - 1) + crossGap
|
|
1183
|
+
case "space-around":
|
|
1184
|
+
crossBetween = free / CGFloat(lineCrosses.count) + crossGap
|
|
1185
|
+
crossLeading = free / CGFloat(lineCrosses.count * 2)
|
|
1186
|
+
case "stretch":
|
|
1187
|
+
let extra = free / CGFloat(lineCrosses.count)
|
|
1188
|
+
lineCrosses = lineCrosses.map { $0 + extra }
|
|
1189
|
+
for (lineIndex, line) in lines.enumerated() {
|
|
1190
|
+
for index in line where items[index].fixedCross == nil && items[index].align == "stretch" {
|
|
1191
|
+
let item = items[index]
|
|
1192
|
+
items[index].cross = clamp(lineCrosses[lineIndex] - item.crossMargins, item.minCross, item.maxCross)
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
default: break
|
|
1196
|
+
}
|
|
662
1197
|
}
|
|
663
1198
|
contentCross = definiteCross
|
|
664
1199
|
}
|
|
@@ -679,23 +1214,36 @@ private final class CraftNativeFlowView: UIStackView {
|
|
|
679
1214
|
default: leading = 0; between = 0
|
|
680
1215
|
}
|
|
681
1216
|
let lineCross = lineCrosses[lineIndex]
|
|
1217
|
+
let lineOffset = sharedCrossOffsets?[lineIndex] ?? crossOffset
|
|
1218
|
+
let sharedLine = sharedLines[lineIndex]
|
|
1219
|
+
let useSharedPositions = !reverse && sharedLine != nil
|
|
682
1220
|
var mainOffset = leading
|
|
683
|
-
for index in line {
|
|
1221
|
+
for (offset, index) in line.enumerated() {
|
|
684
1222
|
let item = items[index]
|
|
685
|
-
let
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1223
|
+
let alignment = item.align == "center" ? UInt32(1) : item.align == "flex-end" ? UInt32(2) : UInt32(0)
|
|
1224
|
+
let crossPosition = CraftNativeCrossAxisBridge.position(
|
|
1225
|
+
available: lineCross,
|
|
1226
|
+
size: item.cross,
|
|
1227
|
+
leading: item.crossLeading,
|
|
1228
|
+
trailing: item.crossTrailing,
|
|
1229
|
+
alignment: alignment
|
|
1230
|
+
) ?? {
|
|
1231
|
+
switch alignment {
|
|
1232
|
+
case 1: return item.crossLeading + (lineCross - item.crossMargins - item.cross) / 2
|
|
1233
|
+
case 2: return lineCross - item.crossTrailing - item.cross
|
|
1234
|
+
default: return item.crossLeading
|
|
1235
|
+
}
|
|
1236
|
+
}()
|
|
1237
|
+
let mainPosition = useSharedPositions
|
|
1238
|
+
? (sharedLine?[offset].position ?? (mainOffset + item.mainLeading))
|
|
1239
|
+
: mainOffset + item.mainLeading
|
|
692
1240
|
let frame = horizontal
|
|
693
|
-
? CGRect(x: padding.left + mainPosition, y: padding.top +
|
|
694
|
-
: CGRect(x: padding.left +
|
|
1241
|
+
? CGRect(x: padding.left + mainPosition, y: padding.top + lineOffset + crossPosition, width: item.main, height: item.cross)
|
|
1242
|
+
: CGRect(x: padding.left + lineOffset + crossPosition, y: padding.top + mainPosition, width: item.cross, height: item.main)
|
|
695
1243
|
frames.append((item.view, frame))
|
|
696
|
-
mainOffset += item.mainMargins + item.main + mainGap + between
|
|
1244
|
+
if !useSharedPositions { mainOffset += item.mainMargins + item.main + mainGap + between }
|
|
697
1245
|
}
|
|
698
|
-
crossOffset += lineCross + crossBetween
|
|
1246
|
+
if sharedCrossOffsets == nil { crossOffset += lineCross + crossBetween }
|
|
699
1247
|
}
|
|
700
1248
|
}
|
|
701
1249
|
return horizontal ? CGSize(width: contentMain, height: contentCross) : CGSize(width: contentCross, height: contentMain)
|
|
@@ -746,7 +1294,35 @@ private final class CraftNativeFlowView: UIStackView {
|
|
|
746
1294
|
}
|
|
747
1295
|
let width = innerWidth ?? (CGFloat(columns) * cellWidth + gaps)
|
|
748
1296
|
let height = rowHeights.reduce(0, +) + CGFloat(max(0, rowHeights.count - 1)) * verticalGap
|
|
1297
|
+
let sharedFrames = CraftNativeGridBridge.frames(
|
|
1298
|
+
availableWidth: innerWidth ?? width,
|
|
1299
|
+
availableHeight: innerHeight ?? height,
|
|
1300
|
+
columns: columns,
|
|
1301
|
+
columnGap: horizontalGap,
|
|
1302
|
+
rowGap: verticalGap,
|
|
1303
|
+
items: children.enumerated().map { index, child in
|
|
1304
|
+
let style = child.craftLayoutStyle
|
|
1305
|
+
let align = style.alignSelf ?? alignItems
|
|
1306
|
+
let size = sizes[index]
|
|
1307
|
+
let alignment: UInt32 = align == "center" ? 1 : align == "flex-end" ? 2 : 0
|
|
1308
|
+
return (
|
|
1309
|
+
size.width, size.height,
|
|
1310
|
+
style.width?.resolve(cellWidth), style.height?.resolve(nil),
|
|
1311
|
+
style.minWidth?.resolve(cellWidth), style.maxWidth?.resolve(cellWidth),
|
|
1312
|
+
style.minHeight?.resolve(nil), style.maxHeight?.resolve(nil),
|
|
1313
|
+
style.marginLeft, style.marginRight, style.marginTop, style.marginBottom,
|
|
1314
|
+
align == "stretch", alignment
|
|
1315
|
+
)
|
|
1316
|
+
}
|
|
1317
|
+
)
|
|
749
1318
|
if place {
|
|
1319
|
+
if let sharedFrames {
|
|
1320
|
+
for (index, child) in children.enumerated() {
|
|
1321
|
+
let frame = sharedFrames[index]
|
|
1322
|
+
frames.append((child, CGRect(x: padding.left + frame.x, y: padding.top + frame.y, width: frame.width, height: frame.height)))
|
|
1323
|
+
}
|
|
1324
|
+
return CGSize(width: width, height: innerHeight ?? height)
|
|
1325
|
+
}
|
|
750
1326
|
var rowTop = padding.top
|
|
751
1327
|
for (index, child) in children.enumerated() {
|
|
752
1328
|
let style = child.craftLayoutStyle
|
|
@@ -775,6 +1351,7 @@ private final class CraftNativeModalView: UIView {
|
|
|
775
1351
|
let contentStack = CraftNativeFlowView()
|
|
776
1352
|
private let blocker = UIView()
|
|
777
1353
|
private var renderedVisible = false
|
|
1354
|
+
private var dismissSuppressedByRequest = false
|
|
778
1355
|
private var animationGeneration = 0
|
|
779
1356
|
var onRequestClose: (() -> Void)?
|
|
780
1357
|
|
|
@@ -796,6 +1373,7 @@ private final class CraftNativeModalView: UIView {
|
|
|
796
1373
|
required init?(coder: NSCoder) { nil }
|
|
797
1374
|
|
|
798
1375
|
func setVisible(_ visible: Bool, opacity: CGFloat, animationType: String?) {
|
|
1376
|
+
if visible { isUserInteractionEnabled = true }
|
|
799
1377
|
if renderedVisible == visible && isHidden == !visible {
|
|
800
1378
|
if visible, alpha != opacity { alpha = opacity }
|
|
801
1379
|
return
|
|
@@ -850,10 +1428,27 @@ private final class CraftNativeModalView: UIView {
|
|
|
850
1428
|
isHidden = true
|
|
851
1429
|
transform = .identity
|
|
852
1430
|
alpha = 0
|
|
1431
|
+
isUserInteractionEnabled = true
|
|
1432
|
+
dismissSuppressedByRequest = false
|
|
853
1433
|
onRequestClose = nil
|
|
854
1434
|
}
|
|
855
1435
|
|
|
856
|
-
|
|
1436
|
+
func requestClose() {
|
|
1437
|
+
guard let onRequestClose else { return }
|
|
1438
|
+
dismissSuppressedByRequest = true
|
|
1439
|
+
isUserInteractionEnabled = false
|
|
1440
|
+
onRequestClose()
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
func consumeDismissSuppression() -> Bool {
|
|
1444
|
+
let suppressed = dismissSuppressedByRequest
|
|
1445
|
+
dismissSuppressedByRequest = false
|
|
1446
|
+
return suppressed
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
func clearDismissSuppression() { dismissSuppressedByRequest = false }
|
|
1450
|
+
|
|
1451
|
+
@objc private func blockerTapped() { requestClose() }
|
|
857
1452
|
|
|
858
1453
|
override func layoutSubviews() {
|
|
859
1454
|
super.layoutSubviews()
|
|
@@ -1532,6 +2127,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
1532
2127
|
/// A drawing's shapes are not views: a change to one redraws its `Svg`.
|
|
1533
2128
|
private var svgOwners: [String: String] = [:]
|
|
1534
2129
|
private let mutationDocument = CraftNativeMutationDocument()
|
|
2130
|
+
private let audio = CraftNativeAudio()
|
|
1535
2131
|
private let capabilityScope = UUID().uuidString
|
|
1536
2132
|
private var pendingCapabilityRequests = Set<String>()
|
|
1537
2133
|
private var pendingCapabilityDeadlines: [String: DispatchWorkItem] = [:]
|
|
@@ -1619,6 +2215,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
1619
2215
|
guard !runtimeDisposed else { return }
|
|
1620
2216
|
runtimeDisposed = true
|
|
1621
2217
|
jsContext.objectForKeyedSubscript("__stxNativeUnmount")?.call(withArguments: [])
|
|
2218
|
+
audio.close()
|
|
1622
2219
|
}
|
|
1623
2220
|
|
|
1624
2221
|
private func refreshTraitDefaults() {
|
|
@@ -1902,6 +2499,76 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
1902
2499
|
keep('navigation', {
|
|
1903
2500
|
setOptions: function(options) { craftNativeSetOptions(JSON.stringify(options || {})); }
|
|
1904
2501
|
});
|
|
2502
|
+
if (typeof globalThis.AudioContext !== 'function') {
|
|
2503
|
+
var nextAudioId = 0;
|
|
2504
|
+
function audioMessage(payload) {
|
|
2505
|
+
globalThis.__stxNativeBridge.postMessage({ type: 'AUDIO', payload: payload });
|
|
2506
|
+
}
|
|
2507
|
+
function audioParam(contextId, nodeId, action, initial) {
|
|
2508
|
+
var current = Number(initial) || 0;
|
|
2509
|
+
var param = {};
|
|
2510
|
+
Object.defineProperty(param, 'value', {
|
|
2511
|
+
configurable: true,
|
|
2512
|
+
enumerable: true,
|
|
2513
|
+
get: function() { return current; },
|
|
2514
|
+
set: function(value) {
|
|
2515
|
+
current = Number(value) || 0;
|
|
2516
|
+
audioMessage({ action: action, contextId: contextId, nodeId: nodeId, value: current });
|
|
2517
|
+
}
|
|
2518
|
+
});
|
|
2519
|
+
function schedule(value, when, ramp) {
|
|
2520
|
+
current = Number(value) || 0;
|
|
2521
|
+
var time = Number(when);
|
|
2522
|
+
audioMessage({ action: action + (ramp ? 'RampToTime' : 'AtTime'), contextId: contextId, nodeId: nodeId, value: current, time: isFinite(time) ? Math.max(0, time) : 0 });
|
|
2523
|
+
}
|
|
2524
|
+
param.setValueAtTime = function(value, when) { schedule(value, when, false); return param; };
|
|
2525
|
+
param.linearRampToValueAtTime = function(value, when) { schedule(value, when, true); return param; };
|
|
2526
|
+
return param;
|
|
2527
|
+
}
|
|
2528
|
+
function AudioContext() {
|
|
2529
|
+
this._id = 'audio-' + (++nextAudioId);
|
|
2530
|
+
this._started = Date.now() / 1000;
|
|
2531
|
+
this.state = 'running';
|
|
2532
|
+
this.destination = { _contextId: this._id, _nodeId: 'destination' };
|
|
2533
|
+
audioMessage({ action: 'createContext', contextId: this._id });
|
|
2534
|
+
}
|
|
2535
|
+
Object.defineProperty(AudioContext.prototype, 'currentTime', {
|
|
2536
|
+
get: function() { return Date.now() / 1000 - this._started; }
|
|
2537
|
+
});
|
|
2538
|
+
AudioContext.prototype.createOscillator = function() {
|
|
2539
|
+
var context = this;
|
|
2540
|
+
var node = { _contextId: context._id, _nodeId: context._id + '-osc-' + (++nextAudioId) };
|
|
2541
|
+
node.frequency = audioParam(context._id, node._nodeId, 'frequency', 440);
|
|
2542
|
+
node.connect = function(destination) {
|
|
2543
|
+
audioMessage({ action: 'connect', contextId: context._id, sourceId: node._nodeId, destinationId: destination && destination._nodeId || 'destination' });
|
|
2544
|
+
return destination;
|
|
2545
|
+
};
|
|
2546
|
+
node.start = function(when) {
|
|
2547
|
+
audioMessage({ action: 'start', contextId: context._id, nodeId: node._nodeId, delay: Math.max(0, (Number(when) || 0) - context.currentTime) });
|
|
2548
|
+
};
|
|
2549
|
+
node.stop = function(when) {
|
|
2550
|
+
audioMessage({ action: 'stop', contextId: context._id, nodeId: node._nodeId, delay: Math.max(0, (Number(when) || 0) - context.currentTime) });
|
|
2551
|
+
};
|
|
2552
|
+
audioMessage({ action: 'createOscillator', contextId: context._id, nodeId: node._nodeId, frequency: node.frequency.value });
|
|
2553
|
+
return node;
|
|
2554
|
+
};
|
|
2555
|
+
AudioContext.prototype.createGain = function() {
|
|
2556
|
+
var context = this;
|
|
2557
|
+
var node = { _contextId: context._id, _nodeId: context._id + '-gain-' + (++nextAudioId) };
|
|
2558
|
+
node.gain = audioParam(context._id, node._nodeId, 'gain', 1);
|
|
2559
|
+
node.connect = function(destination) {
|
|
2560
|
+
audioMessage({ action: 'connect', contextId: context._id, sourceId: node._nodeId, destinationId: destination && destination._nodeId || 'destination' });
|
|
2561
|
+
return destination;
|
|
2562
|
+
};
|
|
2563
|
+
audioMessage({ action: 'createGain', contextId: context._id, nodeId: node._nodeId, gain: node.gain.value });
|
|
2564
|
+
return node;
|
|
2565
|
+
};
|
|
2566
|
+
AudioContext.prototype.resume = function() { this.state = 'running'; return Promise.resolve(); };
|
|
2567
|
+
AudioContext.prototype.suspend = function() { this.state = 'suspended'; return Promise.resolve(); };
|
|
2568
|
+
AudioContext.prototype.close = function() { this.state = 'closed'; audioMessage({ action: 'close', contextId: this._id }); return Promise.resolve(); };
|
|
2569
|
+
globalThis.AudioContext = AudioContext;
|
|
2570
|
+
globalThis.webkitAudioContext = AudioContext;
|
|
2571
|
+
}
|
|
1905
2572
|
})();
|
|
1906
2573
|
""")
|
|
1907
2574
|
}
|
|
@@ -2331,6 +2998,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2331
2998
|
pendingCapabilityRequests.remove(requestToken)
|
|
2332
2999
|
pendingCapabilityDeadlines.removeValue(forKey: requestToken)?.cancel()
|
|
2333
3000
|
CraftNativeActions.cancel(requestToken: requestToken)
|
|
3001
|
+
case "AUDIO":
|
|
3002
|
+
audio.handle(payload)
|
|
2334
3003
|
default:
|
|
2335
3004
|
break
|
|
2336
3005
|
}
|
|
@@ -2402,7 +3071,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2402
3071
|
child.removeFromSuperview()
|
|
2403
3072
|
}
|
|
2404
3073
|
if next.view.superview !== rootStack { rootStack.addArrangedSubview(next.view) }
|
|
2405
|
-
|
|
3074
|
+
// A native screen root fills the safe-area viewport unless the bundle
|
|
3075
|
+
// explicitly sizes it. Without this default, an unstyled SafeAreaView
|
|
3076
|
+
// measures to its scroll content and leaves no viewport to scroll.
|
|
3077
|
+
var rootStyle = next.style
|
|
3078
|
+
if rootStyle["flex"] == nil, rootStyle["height"] == nil { rootStyle["flex"] = 1 }
|
|
3079
|
+
rootStack.setLayoutStyle(rootStyle, for: next.view)
|
|
2406
3080
|
rootStack.invalidateMeasurements()
|
|
2407
3081
|
renderedRoot = next
|
|
2408
3082
|
trackContentScrollView()
|
|
@@ -2565,7 +3239,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2565
3239
|
case "Modal":
|
|
2566
3240
|
let modal = result as! CraftNativeModalView
|
|
2567
3241
|
modal.transparent = props["transparent"] as? Bool == true
|
|
2568
|
-
let visible
|
|
3242
|
+
let visible: Bool
|
|
3243
|
+
if let propVisible = props["visible"] as? Bool {
|
|
3244
|
+
// An explicit visible prop owns modal visibility. The
|
|
3245
|
+
// binding runtime may leave a stale display:none style
|
|
3246
|
+
// from the previous hidden state when it patches props.
|
|
3247
|
+
visible = propVisible
|
|
3248
|
+
} else {
|
|
3249
|
+
let display = style["display"] as? String
|
|
3250
|
+
visible = display != nil && display != "none"
|
|
3251
|
+
}
|
|
2569
3252
|
let modalID = ObjectIdentifier(modal)
|
|
2570
3253
|
let wasVisible = modalVisibility[modalID]
|
|
2571
3254
|
modalVisibility[modalID] = visible
|
|
@@ -2573,9 +3256,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2573
3256
|
modal.onRequestClose = nonEmptyHandler(events["onRequestClose"]).map { handler in
|
|
2574
3257
|
{ [weak self] in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
2575
3258
|
}
|
|
3259
|
+
if visible { modal.clearDismissSuppression() }
|
|
3260
|
+
let suppressDismiss = !visible && modal.consumeDismissSuppression()
|
|
2576
3261
|
if wasVisible != visible, visible || wasVisible == true {
|
|
2577
3262
|
let lifecycleHandler = visible ? events["onShow"] : events["onDismiss"]
|
|
2578
|
-
if let lifecycleHandler = nonEmptyHandler(lifecycleHandler) {
|
|
3263
|
+
if let lifecycleHandler = nonEmptyHandler(lifecycleHandler), !suppressDismiss {
|
|
2579
3264
|
send(type: "EVENT", payload: ["handlerName": lifecycleHandler, "nativeEvent": [:]])
|
|
2580
3265
|
}
|
|
2581
3266
|
}
|
|
@@ -2666,6 +3351,18 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2666
3351
|
updateAuxiliaryHandler(events["onLoadEnd"], in: &imageLoadEndHandlers, for: image)
|
|
2667
3352
|
updateAuxiliaryHandler(events["onError"], in: &imageErrorHandlers, for: image)
|
|
2668
3353
|
updateImage(image, source: props["source"])
|
|
3354
|
+
case "Video":
|
|
3355
|
+
let video = result as! CraftNativeVideoView
|
|
3356
|
+
let autoplay = (props["autoplay"] as? Bool) == true || (props["autoPlay"] as? Bool) == true
|
|
3357
|
+
video.configure(
|
|
3358
|
+
source: props["source"],
|
|
3359
|
+
autoplay: autoplay,
|
|
3360
|
+
paused: (props["paused"] as? Bool) == true,
|
|
3361
|
+
loop: (props["loop"] as? Bool) == true,
|
|
3362
|
+
muted: (props["muted"] as? Bool) == true,
|
|
3363
|
+
controls: (props["controls"] as? Bool) == true,
|
|
3364
|
+
resizeMode: style["resizeMode"] as? String ?? props["resizeMode"] as? String
|
|
3365
|
+
)
|
|
2669
3366
|
case "ScrollView":
|
|
2670
3367
|
let scroll = result as! CraftNativeScrollView
|
|
2671
3368
|
let flexDirection = style["flexDirection"] as? String
|
|
@@ -2772,6 +3469,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2772
3469
|
return UIActivityIndicatorView(style: .medium)
|
|
2773
3470
|
case "Image":
|
|
2774
3471
|
return UIImageView()
|
|
3472
|
+
case "Video":
|
|
3473
|
+
return CraftNativeVideoView(host: self)
|
|
2775
3474
|
case "Icon":
|
|
2776
3475
|
let icon = CraftNativeIconView()
|
|
2777
3476
|
icon.contentMode = .scaleAspectFit
|
|
@@ -2792,7 +3491,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2792
3491
|
}
|
|
2793
3492
|
|
|
2794
3493
|
private func explicitKey(_ node: [String: Any], props: [String: Any]) -> String? {
|
|
2795
|
-
// The current stx native compiler emits key in props; accept the IR
|
|
3494
|
+
// The current @stacksjs/stx native compiler emits key in props; accept the IR
|
|
2796
3495
|
// field too so keyed children keep working when the compiler adopts it.
|
|
2797
3496
|
[node["id"], node["key"], props["key"], props["testID"]]
|
|
2798
3497
|
.compactMap { $0 as? String }
|
|
@@ -3015,6 +3714,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3015
3714
|
imageLoadHandlers.removeValue(forKey: id)
|
|
3016
3715
|
imageLoadEndHandlers.removeValue(forKey: id)
|
|
3017
3716
|
imageErrorHandlers.removeValue(forKey: id)
|
|
3717
|
+
if let video = node.view as? CraftNativeVideoView { video.stop() }
|
|
3018
3718
|
for child in node.children { forgetHandlers(child, preservingInputDrafts: preservingInputDrafts) }
|
|
3019
3719
|
}
|
|
3020
3720
|
|
|
@@ -3261,6 +3961,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3261
3961
|
private func configureStack(_ stack: CraftNativeFlowView, style: [String: Any]) {
|
|
3262
3962
|
let direction = style["flexDirection"] as? String
|
|
3263
3963
|
stack.axis = direction == "row" || direction == "row-reverse" ? .horizontal : .vertical
|
|
3964
|
+
stack.reverse = direction?.hasSuffix("-reverse") == true
|
|
3264
3965
|
stack.wrap = style["flexWrap"] as? String == "wrap" || style["flexWrap"] as? Bool == true
|
|
3265
3966
|
stack.alignItems = style["alignItems"] as? String ?? "stretch"
|
|
3266
3967
|
stack.alignContent = style["alignContent"] as? String ?? "flex-start"
|