@swiftbrowser/native 0.21.0 → 0.25.0
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/package.json +3 -3
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ProgramNode.swift +63 -5
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +954 -126
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Program.swift +137 -41
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +28 -6
- package/swift/Sources/SwiftBrowserProtocol/TextChange.swift +17 -0
- package/wasm/SwiftBrowserNative.wasm +2 -2
|
@@ -8,8 +8,8 @@ import Foundation
|
|
|
8
8
|
/// register, the opcode's operands and, for a stateful builtin, its instance.
|
|
9
9
|
/// A voice may have a right side (`outputRight`), and the `master` stage
|
|
10
10
|
/// runs after the voices (in lane 0). The machine runs `sample` and `master`
|
|
11
|
-
/// a chunk of frames at a time, all but
|
|
12
|
-
///
|
|
11
|
+
/// a chunk of frames at a time, all but the parts that touch state, which it
|
|
12
|
+
/// schedules to run frame by frame when it loads them (block processing).
|
|
13
13
|
public struct DSPProgram: Sendable, Equatable {
|
|
14
14
|
public struct Parameter: Sendable, Equatable {
|
|
15
15
|
public let name: String
|
|
@@ -53,8 +53,6 @@ public struct DSPProgram: Sendable, Equatable {
|
|
|
53
53
|
/// over at every note.
|
|
54
54
|
public struct Master: Sendable, Equatable {
|
|
55
55
|
public let code: [Int32]
|
|
56
|
-
/// The part of `code` that runs frame by frame, in words.
|
|
57
|
-
public let serial: Range<Int>?
|
|
58
56
|
public let inputs: [Int]
|
|
59
57
|
public let outputs: [Int]
|
|
60
58
|
}
|
|
@@ -68,6 +66,10 @@ public struct DSPProgram: Sendable, Equatable {
|
|
|
68
66
|
public let voices: Int
|
|
69
67
|
/// Absent, polyphonic.
|
|
70
68
|
public let mode: Mode?
|
|
69
|
+
/// The voicing as parameters (program.ts `voicing`, their indices), read at each note: how many voices play
|
|
70
|
+
/// (rounded, 1 … `voices`), and the mode (rounded: 0 as many, 1 mono, 2 legato) in place of `mode`.
|
|
71
|
+
public let voicingVoices: Int?
|
|
72
|
+
public let voicingMode: Int?
|
|
71
73
|
public let registers: Int
|
|
72
74
|
public let constants: [(register: Int, value: Float)]
|
|
73
75
|
public let parameters: [Parameter]
|
|
@@ -82,18 +84,50 @@ public struct DSPProgram: Sendable, Equatable {
|
|
|
82
84
|
public let curves: [Curve]
|
|
83
85
|
public let block: [Int32]
|
|
84
86
|
public let sample: [Int32]
|
|
85
|
-
/// The part of `sample` that runs frame by frame, in words.
|
|
86
|
-
public let serial: Range<Int>?
|
|
87
87
|
/// The voice's left side, or both with no `outputRight`.
|
|
88
88
|
public let output: Int
|
|
89
89
|
public let outputRight: Int?
|
|
90
90
|
public let master: Master?
|
|
91
|
+
/// Gated regions (program.ts `groups`): each one's parameter, the region it is inside (-1 none) and its results.
|
|
92
|
+
public let groups: [Gate]
|
|
93
|
+
/// Each instruction's region in `sample` and `master` (-1 none).
|
|
94
|
+
public let sampleGates: [Int32]?
|
|
95
|
+
public let masterGates: [Int32]?
|
|
96
|
+
/// Choices (program.ts `choices`): each a register read as one of its options, the one its parameter names.
|
|
97
|
+
public let choices: [Choice]
|
|
98
|
+
|
|
99
|
+
/// A choice: `register`, which no instruction writes, read as `options[round(parameter's set value × scale)]`
|
|
100
|
+
/// (clamped). What no choice picks, nor anything else needs, the machine does not run.
|
|
101
|
+
public struct Choice: Sendable, Equatable {
|
|
102
|
+
public let parameter: Int
|
|
103
|
+
public let register: Int
|
|
104
|
+
public let options: [Int]
|
|
105
|
+
/// What the parameter's value is multiplied by before it is rounded to an option.
|
|
106
|
+
public var scale: Double = 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// The most options a choice has.
|
|
110
|
+
public static let choiceOptionsMax = 256
|
|
111
|
+
|
|
112
|
+
/// A region: it runs while `parameter` (its index) is set above 0.5 and the region around it runs.
|
|
113
|
+
public struct Gate: Sendable, Equatable {
|
|
114
|
+
public let parameter: Int
|
|
115
|
+
public let parent: Int
|
|
116
|
+
public let outputs: [Output]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// A region's result: a register no instruction writes, read as `on` while the region runs and `off` while not.
|
|
120
|
+
public struct Output: Sendable, Equatable {
|
|
121
|
+
public let register: Int
|
|
122
|
+
public let on: Int
|
|
123
|
+
public let off: Int
|
|
124
|
+
}
|
|
91
125
|
|
|
92
126
|
public static func == (a: DSPProgram, b: DSPProgram) -> Bool {
|
|
93
127
|
a.voices == b.voices && a.mode == b.mode && a.registers == b.registers && a.constants.map(\.register) == b.constants.map(\.register)
|
|
94
128
|
&& a.constants.map(\.value) == b.constants.map(\.value) && a.parameters == b.parameters && a.state == b.state
|
|
95
129
|
&& a.instances == b.instances && a.tables == b.tables && a.shapes == b.shapes && a.samples == b.samples && a.routes == b.routes && a.curves == b.curves
|
|
96
|
-
&& a.block == b.block && a.sample == b.sample && a.
|
|
130
|
+
&& a.block == b.block && a.sample == b.sample && a.output == b.output && a.outputRight == b.outputRight && a.master == b.master
|
|
97
131
|
}
|
|
98
132
|
|
|
99
133
|
public static let format = "swiftbrowser-dsp"
|
|
@@ -116,9 +150,10 @@ public struct DSPProgram: Sendable, Equatable {
|
|
|
116
150
|
/// An envelope's curves: the exponents its attack, decay and release are bent by (1 as without).
|
|
117
151
|
public struct Curve: Equatable, Sendable {
|
|
118
152
|
public let instance: Int
|
|
119
|
-
|
|
120
|
-
public let
|
|
121
|
-
public let
|
|
153
|
+
/// The registers its exponents are read from, each frame (held to 1/16 … 16).
|
|
154
|
+
public let attack: Int
|
|
155
|
+
public let decay: Int
|
|
156
|
+
public let release: Int
|
|
122
157
|
}
|
|
123
158
|
/// The longest a delay may be, in seconds.
|
|
124
159
|
public static let delaySecondsMax: Float = 2
|
|
@@ -126,6 +161,8 @@ public struct DSPProgram: Sendable, Equatable {
|
|
|
126
161
|
public static let shapePointsMax = 4096
|
|
127
162
|
public static let shapesPointsMax = 65_536
|
|
128
163
|
public static let samplesMax = 8_000_000
|
|
164
|
+
/// The most registers a route list has (128 routes): a machine keeps room for a list this long.
|
|
165
|
+
public static let routesMax = 256
|
|
129
166
|
|
|
130
167
|
/// The program the JSON describes, checked: every register, operand and instance in range.
|
|
131
168
|
public init(json: String) throws {
|
|
@@ -279,6 +316,7 @@ public struct DSPProgram: Sendable, Equatable {
|
|
|
279
316
|
guard let lists = routes as? [[Any]] else { throw DSPProgramError("routes must be an array of lists") }
|
|
280
317
|
parsedRoutes = try lists.map { list in
|
|
281
318
|
guard list.count % 2 == 0 else { throw DSPProgramError("a route list is source and amount registers, pair after pair") }
|
|
319
|
+
guard list.count <= Self.routesMax else { throw DSPProgramError("a route list has more than \(Self.routesMax) registers") }
|
|
282
320
|
let registers = try list.map { try register($0, "route") }
|
|
283
321
|
// An amount is a parameter, the same in every lane: the machine tests it once for all of them.
|
|
284
322
|
for k in stride(from: 1, to: registers.count, by: 2) where !parsedParameters.contains(where: { $0.register == registers[k] }) {
|
|
@@ -294,8 +332,7 @@ public struct DSPProgram: Sendable, Equatable {
|
|
|
294
332
|
guard entry.count == 4, let instance = Self.int(entry[0]), instances.indices.contains(instance), instances[instance] == .adsr else {
|
|
295
333
|
throw DSPProgramError("a curve is [instance, attack, decay, release] for an adsr")
|
|
296
334
|
}
|
|
297
|
-
let exponents = try entry.dropFirst().map { try
|
|
298
|
-
guard exponents.allSatisfy({ Self.curveRange.contains($0) }) else { throw DSPProgramError("a curve's exponents are 1/16 … 16") }
|
|
335
|
+
let exponents = try entry.dropFirst().map { try register($0, "curve") }
|
|
299
336
|
return Curve(instance: instance, attack: exponents[0], decay: exponents[1], release: exponents[2])
|
|
300
337
|
}
|
|
301
338
|
}
|
|
@@ -304,33 +341,21 @@ public struct DSPProgram: Sendable, Equatable {
|
|
|
304
341
|
for r in [output, outputRight].compactMap({ $0 }) + (masterParts?.outputs ?? []) where stateRegisters.contains(r) {
|
|
305
342
|
throw DSPProgramError("output register \(r) is state")
|
|
306
343
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
throw DSPProgramError("\(key) is not [from, to)")
|
|
311
|
-
}
|
|
312
|
-
return from..<to
|
|
313
|
-
}
|
|
314
|
-
func code(_ key: String, serial: Range<Int>?) throws -> [Int32] {
|
|
344
|
+
/// Every register an instruction writes.
|
|
345
|
+
var written = Set<Int>()
|
|
346
|
+
func code(_ key: String) throws -> [Int32] {
|
|
315
347
|
guard let words = root[key] as? [Any] else { throw DSPProgramError("\(key) must be an array") }
|
|
316
348
|
let code = try words.map { word -> Int32 in
|
|
317
349
|
guard let n = Self.int(word), n >= Int(Int32.min), n <= Int(Int32.max) else { throw DSPProgramError("\(key): a word is not a whole number") }
|
|
318
350
|
return Int32(n)
|
|
319
351
|
}
|
|
320
|
-
if let serial, serial.upperBound > code.count { throw DSPProgramError("\(key)'s serial part is not [from, to) within it") }
|
|
321
|
-
var boundaries = 0
|
|
322
352
|
var pc = 0
|
|
323
353
|
while pc < code.count {
|
|
324
|
-
if let serial { boundaries += (pc == serial.lowerBound ? 1 : 0) + (pc == serial.upperBound ? 1 : 0) }
|
|
325
354
|
guard let op = DSPOpcode(rawValue: code[pc]) else { throw DSPProgramError("\(key): unknown opcode \(code[pc]) at \(pc)") }
|
|
326
355
|
let length = 2 + op.arity + (op.builtin == nil ? 0 : 1)
|
|
327
356
|
guard pc + length <= code.count else { throw DSPProgramError("\(key): \(op) at \(pc) is cut off") }
|
|
328
357
|
for i in 1..<(2 + op.arity) { _ = try register(Int(code[pc + i]), "\(key) \(op) at \(pc)") }
|
|
329
|
-
|
|
330
|
-
let inSerial = serial?.contains(pc) ?? false
|
|
331
|
-
if !inSerial && (1..<(2 + op.arity)).contains(where: { stateRegisters.contains(Int(code[pc + $0])) }) {
|
|
332
|
-
throw DSPProgramError("\(key): \(op) at \(pc) reads or writes state outside the serial part")
|
|
333
|
-
}
|
|
358
|
+
written.insert(Int(code[pc + 1]))
|
|
334
359
|
if let builtin = op.builtin {
|
|
335
360
|
let instance = Int(code[pc + length - 1])
|
|
336
361
|
guard instances.indices.contains(instance), instances[instance] == builtin else {
|
|
@@ -351,9 +376,6 @@ public struct DSPProgram: Sendable, Equatable {
|
|
|
351
376
|
guard !parsedRoutes[Int(index)].contains(Int(code[pc + 1])) else {
|
|
352
377
|
throw DSPProgramError("\(key): modsum at \(pc) writes a register its routes read")
|
|
353
378
|
}
|
|
354
|
-
guard inSerial || !parsedRoutes[Int(index)].contains(where: { stateRegisters.contains($0) }) else {
|
|
355
|
-
throw DSPProgramError("\(key): modsum at \(pc) reads state outside the serial part")
|
|
356
|
-
}
|
|
357
379
|
} else if let longestAt = op.longest {
|
|
358
380
|
guard let longest = constantValues[Int(code[pc + 1 + longestAt])], longest > 0, longest <= Self.delaySecondsMax else {
|
|
359
381
|
throw DSPProgramError("\(key): \(op) at \(pc)'s longest time is not a constant up to \(Self.delaySecondsMax) s")
|
|
@@ -361,10 +383,6 @@ public struct DSPProgram: Sendable, Equatable {
|
|
|
361
383
|
}
|
|
362
384
|
pc += length
|
|
363
385
|
}
|
|
364
|
-
if let serial {
|
|
365
|
-
boundaries += (pc == serial.lowerBound ? 1 : 0) + (pc == serial.upperBound ? 1 : 0)
|
|
366
|
-
guard boundaries == 2 else { throw DSPProgramError("\(key)'s serial part does not fall between instructions") }
|
|
367
|
-
}
|
|
368
386
|
return code
|
|
369
387
|
}
|
|
370
388
|
self.voices = voices
|
|
@@ -379,14 +397,92 @@ public struct DSPProgram: Sendable, Equatable {
|
|
|
379
397
|
self.samples = parsedSamples
|
|
380
398
|
self.routes = parsedRoutes
|
|
381
399
|
self.curves = parsedCurves
|
|
382
|
-
self.block = try code("block"
|
|
383
|
-
|
|
384
|
-
self.serial = sampleSerial
|
|
385
|
-
self.sample = try code("sample", serial: sampleSerial)
|
|
400
|
+
self.block = try code("block")
|
|
401
|
+
self.sample = try code("sample")
|
|
386
402
|
self.output = output
|
|
387
403
|
self.outputRight = outputRight
|
|
388
|
-
|
|
389
|
-
|
|
404
|
+
self.master = try masterParts.map { Master(code: try code("master"), inputs: $0.inputs, outputs: $0.outputs) }
|
|
405
|
+
// Gated regions: each one's parameter one of the program's, the region around it an earlier one, and its
|
|
406
|
+
// results registers no instruction writes and nothing else names (no output, state, parameter, constant, route
|
|
407
|
+
// or another result), each read as a register while on and one while off, neither of them state (one value a
|
|
408
|
+
// voice, not one a frame).
|
|
409
|
+
var named = Set([output, outputRight].compactMap { $0 } + (masterParts?.inputs ?? []) + (masterParts?.outputs ?? []) + parsedRoutes.flatMap { $0 })
|
|
410
|
+
named.formUnion(stateRegisters)
|
|
411
|
+
named.formUnion(parsedParameters.map(\.register))
|
|
412
|
+
named.formUnion(parsedConstants.map(\.register))
|
|
413
|
+
named.formUnion(parsedCurves.flatMap { [$0.attack, $0.decay, $0.release] })
|
|
414
|
+
var results = Set<Int>()
|
|
415
|
+
var parsedGroups: [Gate] = []
|
|
416
|
+
for (index, item) in (root["groups"] as? [Any] ?? []).enumerated() {
|
|
417
|
+
guard let group = item as? [String: Any], let parameter = Self.int(group["parameter"]), parsedParameters.indices.contains(parameter) else {
|
|
418
|
+
throw DSPProgramError("groups[\(index)] does not name a parameter")
|
|
419
|
+
}
|
|
420
|
+
guard let parent = Self.int(group["parent"]), parent >= -1, parent < index else { throw DSPProgramError("groups[\(index)]'s parent is not an earlier region") }
|
|
421
|
+
guard let list = group["outputs"] as? [Any] else { throw DSPProgramError("groups[\(index)]'s outputs must be an array") }
|
|
422
|
+
let outputs = try list.map { entry -> Output in
|
|
423
|
+
guard let triple = entry as? [Any], triple.count == 3 else { throw DSPProgramError("groups[\(index)]: a result is [register, on, off]") }
|
|
424
|
+
let (r, on, off) = (try register(triple[0], "groups[\(index)]"), try register(triple[1], "groups[\(index)]"), try register(triple[2], "groups[\(index)]"))
|
|
425
|
+
guard r >= Self.machineRegisters, !written.contains(r), !named.contains(r), !results.contains(r) else {
|
|
426
|
+
throw DSPProgramError("groups[\(index)]: result register \(r) is written or named elsewhere")
|
|
427
|
+
}
|
|
428
|
+
guard !stateRegisters.contains(on), !stateRegisters.contains(off) else { throw DSPProgramError("groups[\(index)]: result register \(r) reads as state") }
|
|
429
|
+
results.insert(r)
|
|
430
|
+
return Output(register: r, on: on, off: off)
|
|
431
|
+
}
|
|
432
|
+
parsedGroups.append(Gate(parameter: parameter, parent: parent, outputs: outputs))
|
|
433
|
+
}
|
|
434
|
+
self.groups = parsedGroups
|
|
435
|
+
// Choices: each one's parameter one of the program's, its register one nothing else writes or names (nor a
|
|
436
|
+
// region's result, nor another choice), its options 1 … choiceOptionsMax registers, none of them state.
|
|
437
|
+
let voicing = root["voicing"] as? [String: Any] ?? [:]
|
|
438
|
+
for key in voicing.keys {
|
|
439
|
+
guard ["voices", "mode"].contains(key), let index = Self.int(voicing[key]), parsedParameters.indices.contains(index) else {
|
|
440
|
+
throw DSPProgramError("voicing.\(key) does not name a parameter")
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
self.voicingVoices = Self.int(voicing["voices"])
|
|
444
|
+
self.voicingMode = Self.int(voicing["mode"])
|
|
445
|
+
self.choices = try (root["choices"] as? [Any] ?? []).enumerated().map { index, item in
|
|
446
|
+
guard let choice = item as? [String: Any], let parameter = Self.int(choice["parameter"]), parsedParameters.indices.contains(parameter) else {
|
|
447
|
+
throw DSPProgramError("choices[\(index)] does not name a parameter")
|
|
448
|
+
}
|
|
449
|
+
var scale = 1.0
|
|
450
|
+
if let value = choice["scale"] {
|
|
451
|
+
guard let number = value as? NSNumber, !(value is Bool), number.doubleValue.isFinite, number.doubleValue > 0 else {
|
|
452
|
+
throw DSPProgramError("choices[\(index)]'s scale is not a number above 0")
|
|
453
|
+
}
|
|
454
|
+
scale = number.doubleValue
|
|
455
|
+
}
|
|
456
|
+
let r = try register(choice["register"], "choices[\(index)]")
|
|
457
|
+
guard r >= Self.machineRegisters, !written.contains(r), !named.contains(r), !results.contains(r) else {
|
|
458
|
+
throw DSPProgramError("choices[\(index)]: register \(r) is written or named elsewhere")
|
|
459
|
+
}
|
|
460
|
+
results.insert(r)
|
|
461
|
+
guard let list = choice["options"] as? [Any], (1...Self.choiceOptionsMax).contains(list.count) else {
|
|
462
|
+
throw DSPProgramError("choices[\(index)] has 1 … \(Self.choiceOptionsMax) options")
|
|
463
|
+
}
|
|
464
|
+
let options = try list.map { try register($0, "choices[\(index)]") }
|
|
465
|
+
guard !options.contains(where: stateRegisters.contains) else { throw DSPProgramError("choices[\(index)]: an option is state") }
|
|
466
|
+
return Choice(parameter: parameter, register: r, options: options, scale: scale)
|
|
467
|
+
}
|
|
468
|
+
/// One region (or -1) per instruction of `code`.
|
|
469
|
+
func regions(_ key: String, _ code: [Int32]?) throws -> [Int32]? {
|
|
470
|
+
guard let value = root[key] else { return nil }
|
|
471
|
+
guard let list = value as? [Any], let code else { throw DSPProgramError("\(key) must be an array") }
|
|
472
|
+
var count = 0
|
|
473
|
+
var pc = 0
|
|
474
|
+
while pc < code.count, let op = DSPOpcode(rawValue: code[pc]) {
|
|
475
|
+
count += 1
|
|
476
|
+
pc += 2 + op.arity + (op.builtin == nil ? 0 : 1)
|
|
477
|
+
}
|
|
478
|
+
guard list.count == count else { throw DSPProgramError("\(key) has \(list.count) entries for \(count) instructions") }
|
|
479
|
+
return try list.map { item in
|
|
480
|
+
guard let g = Self.int(item), g >= -1, g < parsedGroups.count else { throw DSPProgramError("\(key): a region is out of range") }
|
|
481
|
+
return Int32(g)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
self.sampleGates = try regions("sampleGates", sample)
|
|
485
|
+
self.masterGates = try regions("masterGates", master?.code)
|
|
390
486
|
}
|
|
391
487
|
|
|
392
488
|
private static func int(_ value: Any?) -> Int? {
|
|
@@ -224,7 +224,10 @@ struct TextEditorElement: View {
|
|
|
224
224
|
#if !canImport(FoundationBridge)
|
|
225
225
|
/// The plain `texteditor` with `selection` (iOS 18, `TextEditor(text:selection:)`):
|
|
226
226
|
/// the viewer's selection reported in UTF-16 offsets, and the caret moved
|
|
227
|
-
/// to one the app sets, as the rich editor does.
|
|
227
|
+
/// to one the app sets, as the rich editor does. A text that arrives from
|
|
228
|
+
/// outside (not the echo of what the viewer typed) keeps the caret: the
|
|
229
|
+
/// app's selection is applied even when its numbers did not change, else
|
|
230
|
+
/// the caret is moved past the change (`caretAfterChange`).
|
|
228
231
|
@available(iOS 18, macOS 15, *)
|
|
229
232
|
struct PlainTextEditorElement: View {
|
|
230
233
|
let node: ElementNode
|
|
@@ -232,16 +235,22 @@ struct PlainTextEditorElement: View {
|
|
|
232
235
|
@State private var selection: TextSelection?
|
|
233
236
|
@State private var shown = Shown()
|
|
234
237
|
|
|
235
|
-
/// The selection last sent, or last moved to (a class: set while drawing).
|
|
238
|
+
/// The selection last sent, or last moved to, and the text last sent (a class: set while drawing).
|
|
236
239
|
final class Shown {
|
|
237
240
|
var sent: TextSelectionSpec?
|
|
241
|
+
var sentText: String?
|
|
238
242
|
}
|
|
239
243
|
|
|
240
244
|
var body: some View {
|
|
241
245
|
TextEditor(
|
|
242
246
|
text: Binding(
|
|
243
247
|
get: { node.props.string("text") ?? "" },
|
|
244
|
-
set: {
|
|
248
|
+
set: { edited in
|
|
249
|
+
if let event = textChange(of: node, to: edited) {
|
|
250
|
+
shown.sentText = edited
|
|
251
|
+
app?.dispatch(event)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
245
254
|
),
|
|
246
255
|
selection: Binding(
|
|
247
256
|
get: { selection },
|
|
@@ -256,12 +265,25 @@ struct PlainTextEditorElement: View {
|
|
|
256
265
|
)
|
|
257
266
|
)
|
|
258
267
|
.onChange(of: TextEditorProps(props: node.props).selection) { _, wanted in move(to: wanted) }
|
|
268
|
+
.onChange(of: node.props.string("text") ?? "") { old, new in textArrived(from: old, to: new) }
|
|
259
269
|
.onAppear { move(to: TextEditorProps(props: node.props).selection) }
|
|
260
270
|
}
|
|
261
271
|
|
|
262
|
-
///
|
|
263
|
-
private func
|
|
264
|
-
|
|
272
|
+
/// The app's text changed: the echo of what the viewer typed leaves the caret alone; a text from outside keeps it in place.
|
|
273
|
+
private func textArrived(from old: String, to new: String) {
|
|
274
|
+
if new == shown.sentText { return }
|
|
275
|
+
if let wanted = TextEditorProps(props: node.props).selection {
|
|
276
|
+
move(to: wanted, force: true)
|
|
277
|
+
} else if let current = selection, let range = offsets(of: current, in: old) {
|
|
278
|
+
let start = stringIndex(utf16: caretAfterChange(before: old, after: new, offset: range.start), in: new)
|
|
279
|
+
let end = stringIndex(utf16: caretAfterChange(before: old, after: new, offset: range.end), in: new)
|
|
280
|
+
selection = start == end ? TextSelection(insertionPoint: start) : TextSelection(range: start..<end)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// A selection the app set (not the one this editor sent): the caret goes there; `force` moves even to the numbers last sent (the text changed under them).
|
|
285
|
+
private func move(to wanted: TextSelectionSpec?, force: Bool = false) {
|
|
286
|
+
guard let wanted, force || wanted != shown.sent else { return }
|
|
265
287
|
shown.sent = wanted
|
|
266
288
|
let text = node.props.string("text") ?? ""
|
|
267
289
|
let start = stringIndex(utf16: wanted.start, in: text)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/// Where a caret goes when a text changes under it from outside (another
|
|
2
|
+
/// person's edit arriving in a `texteditor`): an offset before the change
|
|
3
|
+
/// (the common prefix of the two texts) stays; one after it (the common
|
|
4
|
+
/// suffix) moves by the change's length; one inside the change goes to the
|
|
5
|
+
/// change's end. Offsets are UTF-16 units, as a `selection` is on the wire.
|
|
6
|
+
public func caretAfterChange(before: String, after: String, offset: Int) -> Int {
|
|
7
|
+
let a = Array(before.utf16)
|
|
8
|
+
let b = Array(after.utf16)
|
|
9
|
+
let shortest = min(a.count, b.count)
|
|
10
|
+
var prefix = 0
|
|
11
|
+
while prefix < shortest, a[prefix] == b[prefix] { prefix += 1 }
|
|
12
|
+
var suffix = 0
|
|
13
|
+
while suffix < shortest - prefix, a[a.count - 1 - suffix] == b[b.count - 1 - suffix] { suffix += 1 }
|
|
14
|
+
if offset <= prefix { return offset }
|
|
15
|
+
if offset >= a.count - suffix { return offset + (b.count - a.count) }
|
|
16
|
+
return b.count - suffix
|
|
17
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/wasm/SwiftBrowserNative.wasm
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 50265117 bytes
|
|
4
|
+
sha256: f7cb92276a436e8c765a5eca556c7436d02752c411256b0d6ff4e2ca33950635
|