react-x11 2.2.1 → 2.3.1

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.
@@ -0,0 +1,679 @@
1
+ // A canvas-shaped 2d context over a @windowkit/appkit CoreGraphics surface.
2
+ //
3
+ // This is the raster half of the Cocoa backend: on the surface presenter it
4
+ // is the whole drawing path, and on the layer presenter it stays as the
5
+ // fallback every painted-code node (<canvas>, <svg>, registered elements)
6
+ // rasters through — docs/macos.md §"Custom drawing on a layer tree".
7
+ //
8
+ // The native surface holds the real graphics state (paths, CTM, clip); this
9
+ // class keeps the JS-visible state (fillStyle strings, gradient objects,
10
+ // dash arrays) and re-syncs it when the backing surface is replaced after a
11
+ // resize — `_gen` is that generation.
12
+ import { cssColorStraight } from 'ntk';
13
+
14
+ const BLACK = [0, 0, 0, 1];
15
+
16
+ function parseColor(value) {
17
+ if (value == null) return BLACK;
18
+ return cssColorStraight(String(value)) ?? BLACK;
19
+ }
20
+
21
+ class LinearGradient {
22
+ constructor(x0, y0, x1, y1) {
23
+ this._coords = [x0, y0, x1, y1];
24
+ this._stops = [];
25
+ }
26
+
27
+ addColorStop(offset, color) {
28
+ const [r, g, b, a] = parseColor(color);
29
+ this._stops.push(offset, r, g, b, a);
30
+ }
31
+
32
+ /**
33
+ * CoreGraphics requires stop locations inside [0, 1]; the decorations
34
+ * parser deliberately pads a gradient's line past both ends (its end
35
+ * colours pinned there — see src/decorations.js). Out-of-range locations
36
+ * fed to CGGradient render as a solid block, so the line is re-derived:
37
+ * the coordinates extend to cover the outermost stops and every location
38
+ * remaps into [0, 1].
39
+ */
40
+ _normalized() {
41
+ const stops = [];
42
+ for (let i = 0; i + 4 < this._stops.length; i += 5) {
43
+ stops.push(this._stops.slice(i, i + 5));
44
+ }
45
+ stops.sort((p, q) => p[0] - q[0]);
46
+ if (stops.length === 0) return { coords: this._coords, flat: [] };
47
+ if (stops.length === 1) stops.push([...stops[0]]);
48
+ const min = Math.min(0, stops[0][0]);
49
+ const max = Math.max(1, stops[stops.length - 1][0]);
50
+ let [x0, y0, x1, y1] = this._coords;
51
+ if (min !== 0 || max !== 1) {
52
+ const dx = x1 - x0;
53
+ const dy = y1 - y0;
54
+ const nx0 = x0 + dx * min;
55
+ const ny0 = y0 + dy * min;
56
+ x1 = x0 + dx * max;
57
+ y1 = y0 + dy * max;
58
+ x0 = nx0;
59
+ y0 = ny0;
60
+ const span = max - min;
61
+ for (const stop of stops) stop[0] = (stop[0] - min) / span;
62
+ }
63
+ for (const stop of stops) stop[0] = Math.min(1, Math.max(0, stop[0]));
64
+ return { coords: [x0, y0, x1, y1], flat: stops.flat() };
65
+ }
66
+ }
67
+
68
+ export class CocoaContext2D {
69
+ /**
70
+ * @param native the @windowkit/appkit module
71
+ * @param surfaceOf () => current surface handle — the owner replaces the
72
+ * surface on resize, and this context follows it.
73
+ * @param genOf () => surface generation number
74
+ */
75
+ constructor(native, surfaceOf, genOf) {
76
+ this._native = native;
77
+ this._surfaceOf = surfaceOf;
78
+ this._genOf = genOf;
79
+ this._gen = -1;
80
+ this._stack = [];
81
+ this._state = {
82
+ fillStyle: '#000',
83
+ strokeStyle: '#000',
84
+ lineWidth: 1,
85
+ lineCap: 'butt',
86
+ lineJoin: 'miter',
87
+ globalAlpha: 1,
88
+ dash: [],
89
+ dashOffset: 0,
90
+ font: '10px sans-serif',
91
+ shadowBlur: 0,
92
+ shadowOffsetX: 0,
93
+ shadowOffsetY: 0,
94
+ shadowColor: 'rgba(0,0,0,0)',
95
+ ctm: [1, 0, 0, 1, 0, 0],
96
+ };
97
+ this._onDirty = null;
98
+ }
99
+
100
+ _s() {
101
+ const surface = this._surfaceOf();
102
+ const gen = this._genOf();
103
+ if (gen !== this._gen) {
104
+ // fresh surface: push the sticky state back into it
105
+ this._gen = gen;
106
+ const n = this._native;
107
+ const st = this._state;
108
+ n.ctxSetLineWidth(surface, st.lineWidth);
109
+ n.ctxSetLineCap(surface, st.lineCap);
110
+ n.ctxSetLineJoin(surface, st.lineJoin);
111
+ n.ctxSetGlobalAlpha(surface, st.globalAlpha);
112
+ n.ctxSetLineDash(surface, st.dash, st.dashOffset);
113
+ this._stack.length = 0;
114
+ }
115
+ return surface;
116
+ }
117
+
118
+ _dirty() {
119
+ this._onDirty?.();
120
+ }
121
+
122
+ // --- state ---------------------------------------------------------------
123
+
124
+ get fillStyle() {
125
+ return this._state.fillStyle;
126
+ }
127
+
128
+ set fillStyle(value) {
129
+ this._state.fillStyle = value;
130
+ }
131
+
132
+ get strokeStyle() {
133
+ return this._state.strokeStyle;
134
+ }
135
+
136
+ set strokeStyle(value) {
137
+ this._state.strokeStyle = value;
138
+ }
139
+
140
+ get lineWidth() {
141
+ return this._state.lineWidth;
142
+ }
143
+
144
+ set lineWidth(value) {
145
+ if (typeof value === 'number' && value > 0) {
146
+ this._state.lineWidth = value;
147
+ this._native.ctxSetLineWidth(this._s(), value);
148
+ }
149
+ }
150
+
151
+ get lineCap() {
152
+ return this._state.lineCap;
153
+ }
154
+
155
+ set lineCap(value) {
156
+ this._state.lineCap = value;
157
+ this._native.ctxSetLineCap(this._s(), String(value));
158
+ }
159
+
160
+ get lineJoin() {
161
+ return this._state.lineJoin;
162
+ }
163
+
164
+ set lineJoin(value) {
165
+ this._state.lineJoin = value;
166
+ this._native.ctxSetLineJoin(this._s(), String(value));
167
+ }
168
+
169
+ get globalAlpha() {
170
+ return this._state.globalAlpha;
171
+ }
172
+
173
+ set globalAlpha(value) {
174
+ if (typeof value === 'number' && value >= 0 && value <= 1) {
175
+ this._state.globalAlpha = value;
176
+ this._native.ctxSetGlobalAlpha(this._s(), value);
177
+ }
178
+ }
179
+
180
+ get font() {
181
+ return this._state.font;
182
+ }
183
+
184
+ set font(value) {
185
+ this._state.font = String(value);
186
+ }
187
+
188
+ get shadowBlur() {
189
+ return this._state.shadowBlur;
190
+ }
191
+
192
+ set shadowBlur(value) {
193
+ if (typeof value === 'number' && value >= 0) {
194
+ this._state.shadowBlur = value;
195
+ this._syncShadow();
196
+ }
197
+ }
198
+
199
+ get shadowOffsetX() {
200
+ return this._state.shadowOffsetX;
201
+ }
202
+
203
+ set shadowOffsetX(value) {
204
+ if (typeof value === 'number') {
205
+ this._state.shadowOffsetX = value;
206
+ this._syncShadow();
207
+ }
208
+ }
209
+
210
+ get shadowOffsetY() {
211
+ return this._state.shadowOffsetY;
212
+ }
213
+
214
+ set shadowOffsetY(value) {
215
+ if (typeof value === 'number') {
216
+ this._state.shadowOffsetY = value;
217
+ this._syncShadow();
218
+ }
219
+ }
220
+
221
+ get shadowColor() {
222
+ return this._state.shadowColor;
223
+ }
224
+
225
+ set shadowColor(value) {
226
+ this._state.shadowColor = value;
227
+ this._syncShadow();
228
+ }
229
+
230
+ _syncShadow() {
231
+ const st = this._state;
232
+ const [r, g, b, a] = parseColor(st.shadowColor);
233
+ const on = st.shadowBlur > 0 && a > 0;
234
+ this._native.ctxSetShadow(
235
+ this._s(),
236
+ on ? st.shadowBlur : 0,
237
+ st.shadowOffsetX,
238
+ st.shadowOffsetY,
239
+ r,
240
+ g,
241
+ b,
242
+ a,
243
+ );
244
+ }
245
+
246
+ setLineDash(segments) {
247
+ this._state.dash = Array.isArray(segments) ? segments : [];
248
+ this._native.ctxSetLineDash(this._s(), this._state.dash, 0);
249
+ }
250
+
251
+ getLineDash() {
252
+ return [...this._state.dash];
253
+ }
254
+
255
+ save() {
256
+ this._stack.push({ ...this._state, dash: [...this._state.dash] });
257
+ this._native.ctxSave(this._s());
258
+ }
259
+
260
+ restore() {
261
+ const prev = this._stack.pop();
262
+ if (prev) this._state = prev;
263
+ this._native.ctxRestore(this._s());
264
+ }
265
+
266
+ _concat(a2, b2, c2, d2, e2, f2) {
267
+ const [a, b, c, d, e, f] = this._state.ctm;
268
+ this._state.ctm = [
269
+ a * a2 + c * b2,
270
+ b * a2 + d * b2,
271
+ a * c2 + c * d2,
272
+ b * c2 + d * d2,
273
+ a * e2 + c * f2 + e,
274
+ b * e2 + d * f2 + f,
275
+ ];
276
+ }
277
+
278
+ translate(x, y) {
279
+ this._concat(1, 0, 0, 1, x, y);
280
+ this._native.ctxTranslate(this._s(), x, y);
281
+ }
282
+
283
+ scale(x, y) {
284
+ this._concat(x, 0, 0, y, 0, 0);
285
+ this._native.ctxScale(this._s(), x, y);
286
+ }
287
+
288
+ rotate(angle) {
289
+ const cos = Math.cos(angle);
290
+ const sin = Math.sin(angle);
291
+ this._concat(cos, sin, -sin, cos, 0, 0);
292
+ this._native.ctxRotate(this._s(), angle);
293
+ }
294
+
295
+ transform(a, b, c, d, e, f) {
296
+ this._concat(a, b, c, d, e, f);
297
+ this._native.ctxTransform(this._s(), a, b, c, d, e, f);
298
+ }
299
+
300
+ setTransform(a, b, c, d, e, f) {
301
+ if (typeof a === 'object' && a) ({ a, b, c, d, e, f } = a);
302
+ // concat the delta that takes the current matrix to the requested one
303
+ const [ca, cb, cc, cd, ce, cf] = this._state.ctm;
304
+ const det = ca * cd - cb * cc;
305
+ if (!det) return;
306
+ const ia = cd / det;
307
+ const ib = -cb / det;
308
+ const ic = -cc / det;
309
+ const id = ca / det;
310
+ const ie = -(ia * ce + ic * cf);
311
+ const iff = -(ib * ce + id * cf);
312
+ this.transform(
313
+ ia * a + ic * b,
314
+ ib * a + id * b,
315
+ ia * c + ic * d,
316
+ ib * c + id * d,
317
+ ia * e + ic * f + ie,
318
+ ib * e + id * f + iff,
319
+ );
320
+ }
321
+
322
+ resetTransform() {
323
+ this.setTransform(1, 0, 0, 1, 0, 0);
324
+ }
325
+
326
+ getTransform() {
327
+ const [a, b, c, d, e, f] = this._state.ctm;
328
+ return { a, b, c, d, e, f };
329
+ }
330
+
331
+ // --- paths ---------------------------------------------------------------
332
+
333
+ beginPath() {
334
+ this._native.ctxBeginPath(this._s());
335
+ }
336
+
337
+ moveTo(x, y) {
338
+ this._native.ctxMoveTo(this._s(), x, y);
339
+ }
340
+
341
+ lineTo(x, y) {
342
+ this._native.ctxLineTo(this._s(), x, y);
343
+ }
344
+
345
+ rect(x, y, w, h) {
346
+ this._native.ctxRect(this._s(), x, y, w, h);
347
+ }
348
+
349
+ roundRect(x, y, w, h, radii) {
350
+ let r = radii ?? 0;
351
+ if (typeof r === 'number') r = [r, r, r, r];
352
+ else if (r.length === 1) r = [r[0], r[0], r[0], r[0]];
353
+ else if (r.length === 2) r = [r[0], r[1], r[0], r[1]];
354
+ else if (r.length === 3) r = [r[0], r[1], r[2], r[1]];
355
+ const cap = Math.min(Math.abs(w) / 2, Math.abs(h) / 2);
356
+ const clamp = (v) => Math.max(0, Math.min(Number(v) || 0, cap));
357
+ this._native.ctxRoundRect(
358
+ this._s(),
359
+ x,
360
+ y,
361
+ w,
362
+ h,
363
+ clamp(r[0]),
364
+ clamp(r[1]),
365
+ clamp(r[2]),
366
+ clamp(r[3]),
367
+ );
368
+ }
369
+
370
+ arc(x, y, radius, start, end, anticlockwise = false) {
371
+ this._native.ctxArc(this._s(), x, y, radius, start, end, anticlockwise);
372
+ }
373
+
374
+ ellipse(x, y, rx, ry) {
375
+ this._native.ctxEllipse(this._s(), x, y, rx, ry);
376
+ }
377
+
378
+ bezierCurveTo(c1x, c1y, c2x, c2y, x, y) {
379
+ this._native.ctxCurveTo(this._s(), c1x, c1y, c2x, c2y, x, y);
380
+ }
381
+
382
+ quadraticCurveTo(cx, cy, x, y) {
383
+ this._native.ctxQuadTo(this._s(), cx, cy, x, y);
384
+ }
385
+
386
+ closePath() {
387
+ this._native.ctxClosePath(this._s());
388
+ }
389
+
390
+ // --- painting ------------------------------------------------------------
391
+
392
+ createLinearGradient(x0, y0, x1, y1) {
393
+ return new LinearGradient(x0, y0, x1, y1);
394
+ }
395
+
396
+ createRadialGradient() {
397
+ // radial paints flat until someone needs it; the stop list still works
398
+ return new LinearGradient(0, 0, 0, 0);
399
+ }
400
+
401
+ _applyFill() {
402
+ const [r, g, b, a] = parseColor(this._state.fillStyle);
403
+ this._native.ctxSetFillColor(this._s(), r, g, b, a);
404
+ }
405
+
406
+ _applyStroke() {
407
+ const [r, g, b, a] = parseColor(this._state.strokeStyle);
408
+ this._native.ctxSetStrokeColor(this._s(), r, g, b, a);
409
+ }
410
+
411
+ /**
412
+ * Replay an ntk/canvas Path2D (normalized M/L/C/Q/Z commands on `_cmds`)
413
+ * into the native context path. The fill/stroke/clip overloads that take
414
+ * a path argument route through this — ignoring the argument would run
415
+ * the operation on whatever path a PREVIOUS painter left behind, which
416
+ * is how a 44px SVG icon once filled a whole card with its accent.
417
+ */
418
+ _replayPath(path) {
419
+ const cmds = path?._cmds;
420
+ if (!Array.isArray(cmds)) return false;
421
+ const n = this._native;
422
+ const s = this._s();
423
+ n.ctxBeginPath(s);
424
+ for (const c of cmds) {
425
+ if (c.type === 'M') n.ctxMoveTo(s, c.x, c.y);
426
+ else if (c.type === 'L') n.ctxLineTo(s, c.x, c.y);
427
+ else if (c.type === 'C')
428
+ n.ctxCurveTo(s, c.x1, c.y1, c.x2, c.y2, c.x, c.y);
429
+ else if (c.type === 'Q') n.ctxQuadTo(s, c.x1, c.y1, c.x, c.y);
430
+ else if (c.type === 'Z') n.ctxClosePath(s);
431
+ }
432
+ return true;
433
+ }
434
+
435
+ fill(pathOrRule, maybeRule) {
436
+ const hasPath = pathOrRule != null && typeof pathOrRule === 'object';
437
+ const rule = hasPath ? maybeRule : pathOrRule;
438
+ if (hasPath && !this._replayPath(pathOrRule)) return;
439
+ const style = this._state.fillStyle;
440
+ if (style instanceof LinearGradient) {
441
+ const { coords, flat } = style._normalized();
442
+ this._native.ctxFillLinearGradient(
443
+ this._s(),
444
+ coords[0],
445
+ coords[1],
446
+ coords[2],
447
+ coords[3],
448
+ flat,
449
+ );
450
+ } else {
451
+ this._applyFill();
452
+ this._native.ctxFill(this._s(), rule === 'evenodd');
453
+ }
454
+ this._dirty();
455
+ }
456
+
457
+ stroke(path) {
458
+ if (path != null && typeof path === 'object' && !this._replayPath(path)) {
459
+ return;
460
+ }
461
+ this._applyStroke();
462
+ this._native.ctxStroke(this._s());
463
+ this._dirty();
464
+ }
465
+
466
+ clip(pathOrRule) {
467
+ if (
468
+ pathOrRule != null &&
469
+ typeof pathOrRule === 'object' &&
470
+ !this._replayPath(pathOrRule)
471
+ ) {
472
+ return;
473
+ }
474
+ this._native.ctxClip(this._s());
475
+ }
476
+
477
+ fillRect(x, y, w, h) {
478
+ if (!(w > 0) || !(h > 0)) return;
479
+ const style = this._state.fillStyle;
480
+ if (style instanceof LinearGradient) {
481
+ const { coords, flat } = style._normalized();
482
+ this._native.ctxFillLinearGradient(
483
+ this._s(),
484
+ coords[0],
485
+ coords[1],
486
+ coords[2],
487
+ coords[3],
488
+ flat,
489
+ x,
490
+ y,
491
+ w,
492
+ h,
493
+ );
494
+ } else {
495
+ this._applyFill();
496
+ this._native.ctxFillRect(this._s(), x, y, w, h);
497
+ }
498
+ this._dirty();
499
+ }
500
+
501
+ fillRects(rects) {
502
+ const flat = Array.isArray(rects?.[0]) ? rects.flat() : (rects ?? []);
503
+ if (!flat.length) return;
504
+ this._applyFill();
505
+ this._native.ctxFillRects(this._s(), flat);
506
+ this._dirty();
507
+ }
508
+
509
+ strokeRect(x, y, w, h) {
510
+ this._applyStroke();
511
+ this._native.ctxStrokeRect(this._s(), x, y, w, h);
512
+ this._dirty();
513
+ }
514
+
515
+ clearRect(x, y, w, h) {
516
+ this._native.ctxClearRect(this._s(), x, y, w, h);
517
+ this._dirty();
518
+ }
519
+
520
+ drawImage(image, ...args) {
521
+ const src = image?._surfaceHandle ?? image?._surface?._surfaceHandle;
522
+ if (!src) return; // ntk Images/Pictures are not on this backend yet
523
+ const size = this._native.surfaceSize(src);
524
+ let sx = 0;
525
+ let sy = 0;
526
+ let sw = size.width;
527
+ let sh = size.height;
528
+ let dx;
529
+ let dy;
530
+ let dw;
531
+ let dh;
532
+ if (args.length >= 8) {
533
+ [sx, sy, sw, sh, dx, dy, dw, dh] = args;
534
+ } else if (args.length >= 4) {
535
+ [dx, dy, dw, dh] = args;
536
+ } else {
537
+ [dx, dy] = args;
538
+ dw = sw;
539
+ dh = sh;
540
+ }
541
+ this._native.ctxDrawSurface(this._s(), src, sx, sy, sw, sh, dx, dy, dw, dh);
542
+ this._dirty();
543
+ }
544
+
545
+ /**
546
+ * Browser contract: a blank RGBA pixel block for the caller to fill and
547
+ * hand back to putImageData. Pure allocation — nothing touches the
548
+ * surface — but it lives on the context because that is where every
549
+ * canvas consumer looks for it (the Frame pane's mandelbrot does).
550
+ */
551
+ createImageData(width, height) {
552
+ const w = Math.max(1, Math.round(width));
553
+ const h = Math.max(1, Math.round(height));
554
+ return {
555
+ data: new Uint8ClampedArray(w * h * 4),
556
+ width: w,
557
+ height: h,
558
+ };
559
+ }
560
+
561
+ putImageData(data, x, y) {
562
+ if (!data?.data) return;
563
+ const buf = Buffer.isBuffer(data.data)
564
+ ? data.data
565
+ : Buffer.from(data.data.buffer ?? data.data);
566
+ this._native.ctxPutImageData(
567
+ this._s(),
568
+ buf,
569
+ data.width,
570
+ data.height,
571
+ Math.round(x),
572
+ Math.round(y),
573
+ );
574
+ this._dirty();
575
+ }
576
+
577
+ /**
578
+ * ntk's contract, not the browser's: with a callback it delivers
579
+ * `(err, imageData)`; without one it returns a Promise. On X11 the read
580
+ * is a server round trip, so every consumer in the tree is written
581
+ * async — the configurator's screen capture, the pixel harness,
582
+ * scripts/capture.js — and a backend that answered synchronously would
583
+ * strand their callbacks unfired. The pixels are read at call time (the
584
+ * surface only changes in the pump, which cannot run before a
585
+ * microtask), the delivery is a tick later like a resolved promise's.
586
+ */
587
+ getImageData(x, y, w, h, cb) {
588
+ const read = () => {
589
+ const buf = this._native.ctxGetImageData(
590
+ this._s(),
591
+ Math.round(x),
592
+ Math.round(y),
593
+ Math.round(w),
594
+ Math.round(h),
595
+ );
596
+ return {
597
+ data: new Uint8ClampedArray(buf.buffer, buf.byteOffset, buf.length),
598
+ width: Math.round(w),
599
+ height: Math.round(h),
600
+ };
601
+ };
602
+ let result;
603
+ let failure;
604
+ try {
605
+ result = read();
606
+ } catch (err) {
607
+ failure = err;
608
+ }
609
+ const promise = failure ? Promise.reject(failure) : Promise.resolve(result);
610
+ if (typeof cb === 'function') {
611
+ promise.then(
612
+ (data) => cb(null, data),
613
+ (err) => cb(err),
614
+ );
615
+ return undefined;
616
+ }
617
+ return promise;
618
+ }
619
+
620
+ // --- text (minimal: enough for <canvas onDraw> users) --------------------
621
+
622
+ _drawLayout(layout, x, y) {
623
+ if (layout._contextInk) {
624
+ const style = this._state.fillStyle;
625
+ if (style instanceof LinearGradient) {
626
+ const { coords, flat } = style._normalized();
627
+ this._native.drawLayoutGradient(
628
+ this._s(),
629
+ layout._handle,
630
+ x,
631
+ y,
632
+ coords[0],
633
+ coords[1],
634
+ coords[2],
635
+ coords[3],
636
+ flat,
637
+ );
638
+ this._dirty();
639
+ return;
640
+ }
641
+ this._applyFill();
642
+ }
643
+ this._native.drawLayout(this._s(), layout._handle, x, y);
644
+ this._dirty();
645
+ }
646
+
647
+ measureText(text) {
648
+ const layout = this._fontLayout(text);
649
+ return layout
650
+ ? { width: layout.width }
651
+ : { width: String(text).length * 7 };
652
+ }
653
+
654
+ fillText(text, x, y) {
655
+ const layout = this._fontLayout(text);
656
+ if (!layout) return;
657
+ // canvas fillText's y is the baseline; layouts draw from their top
658
+ const baseline = layout.lines[0]?.baseline ?? 0;
659
+ this._drawLayout(layout, x, y - baseline);
660
+ }
661
+
662
+ _fontLayout(text, color) {
663
+ const fonts = this._fonts;
664
+ if (!fonts) return null;
665
+ const m =
666
+ /^(?:(italic|oblique)\s+)?(?:(\d{3}|bold)\s+)?(\d+(?:\.\d+)?)px\s+(.+)$/.exec(
667
+ this._state.font,
668
+ );
669
+ const size = m ? Number(m[3]) : 12;
670
+ const family = m ? m[4] : 'sans-serif';
671
+ const weight = m?.[2] === 'bold' ? 700 : m?.[2] ? Number(m[2]) : 400;
672
+ const style = m?.[1] ? 'italic' : 'normal';
673
+ return fonts.layout(
674
+ [{ text: String(text), family, size, weight, style, color }],
675
+ { family, size, weight, style, color: color ?? '#000' },
676
+ {},
677
+ );
678
+ }
679
+ }