@swifttui/web 0.0.27 → 0.1.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.
- package/README.md +58 -43
- package/dist/src/AccessibilityTree.js +23 -14
- package/dist/src/AccessibilityTree.js.map +1 -1
- package/dist/src/CanvasSurfacePainter.js +263 -0
- package/dist/src/CanvasSurfacePainter.js.map +1 -0
- package/dist/src/InputEventEncoder.js +117 -0
- package/dist/src/InputEventEncoder.js.map +1 -0
- package/dist/src/PointerGeometry.js +72 -0
- package/dist/src/PointerGeometry.js.map +1 -0
- package/dist/src/WebHostSceneRuntime.d.ts +11 -20
- package/dist/src/WebHostSceneRuntime.js +40 -359
- package/dist/src/WebHostSceneRuntime.js.map +1 -1
- package/dist/src/wasi/SharedInputQueue.js +14 -8
- package/dist/src/wasi/SharedInputQueue.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { applyWebHostTerminalStyle, normalizeWebHostTerminalStyle, webTUITerminalBackgroundColor } from "./WebHostTerminalStyle.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { CanvasSurfacePainter, fontForStyle } from "./CanvasSurfacePainter.js";
|
|
3
|
+
import { InputEventEncoder } from "./InputEventEncoder.js";
|
|
4
|
+
import { cellLocationForEvent, rawCellLocationForEvent, wheelTargetCanScroll } from "./PointerGeometry.js";
|
|
4
5
|
import { AccessibilityTreeMounter } from "./AccessibilityTree.js";
|
|
5
6
|
//#region src/WebHostSceneRuntime.ts
|
|
6
7
|
/**
|
|
@@ -13,6 +14,13 @@ function legacyWheelMode(captureWheelInput) {
|
|
|
13
14
|
if (captureWheelInput === void 0) return "chain";
|
|
14
15
|
return captureWheelInput ? "capture" : "passive";
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Coordinates a single SwiftTUI scene's browser presentation: it owns the DOM
|
|
19
|
+
* mount, canvas, accessibility tree, and bridge wiring, and delegates the heavy
|
|
20
|
+
* responsibilities to focused collaborators — {@link CanvasSurfacePainter} for
|
|
21
|
+
* canvas drawing, {@link InputEventEncoder} for wire-message encoding, and the
|
|
22
|
+
* {@link PointerGeometry} helpers for pixel→cell hit-testing and wheel chaining.
|
|
23
|
+
*/
|
|
16
24
|
var WebHostSceneRuntime = class {
|
|
17
25
|
descriptor;
|
|
18
26
|
element;
|
|
@@ -22,7 +30,8 @@ var WebHostSceneRuntime = class {
|
|
|
22
30
|
onFrameDiagnostic;
|
|
23
31
|
synchronizeAccessibilityFocus;
|
|
24
32
|
wheelMode;
|
|
25
|
-
|
|
33
|
+
painter = new CanvasSurfacePainter();
|
|
34
|
+
inputEncoder = new InputEventEncoder();
|
|
26
35
|
currentStyle;
|
|
27
36
|
canvas;
|
|
28
37
|
accessibilityTree;
|
|
@@ -66,6 +75,7 @@ var WebHostSceneRuntime = class {
|
|
|
66
75
|
canvas.className = "webhost-scene__surface";
|
|
67
76
|
canvas.setAttribute("aria-hidden", "true");
|
|
68
77
|
this.canvas = canvas;
|
|
78
|
+
this.painter.attach(canvas, () => this.draw());
|
|
69
79
|
this.accessibilityTree = new AccessibilityTreeMounter();
|
|
70
80
|
this.terminalMount.replaceChildren(canvas, this.accessibilityTree.element, this.accessibilityTree.announcerElement);
|
|
71
81
|
this.installInputHandlers();
|
|
@@ -181,35 +191,26 @@ var WebHostSceneRuntime = class {
|
|
|
181
191
|
installInputHandlers() {
|
|
182
192
|
const handleKeyDown = (event) => {
|
|
183
193
|
if (event.metaKey || event.isComposing) return;
|
|
184
|
-
const
|
|
185
|
-
if (!
|
|
186
|
-
this.onInput(
|
|
187
|
-
...key,
|
|
188
|
-
modifiers: modifierMask(event)
|
|
189
|
-
}));
|
|
194
|
+
const message = this.inputEncoder.encodeKey(event);
|
|
195
|
+
if (!message) return;
|
|
196
|
+
this.onInput(message);
|
|
190
197
|
event.preventDefault();
|
|
191
198
|
};
|
|
192
199
|
const handlePaste = (event) => {
|
|
193
200
|
const text = event.clipboardData?.getData("text/plain") ?? "";
|
|
194
201
|
if (!text) return;
|
|
195
|
-
this.onInput(
|
|
202
|
+
this.onInput(this.inputEncoder.encodePaste(text));
|
|
196
203
|
event.preventDefault();
|
|
197
204
|
};
|
|
198
205
|
const handlePointerDown = (event) => {
|
|
199
206
|
const location = this.cellLocation(event);
|
|
200
207
|
if (!location) return;
|
|
201
|
-
const button = pointerButton(event.button);
|
|
208
|
+
const button = this.inputEncoder.pointerButton(event.button);
|
|
202
209
|
this.activePointerButton = button;
|
|
203
210
|
this.hasCapturedPointer = true;
|
|
204
211
|
this.terminalMount.focus?.({ preventScroll: true });
|
|
205
212
|
this.terminalMount.setPointerCapture?.(event.pointerId);
|
|
206
|
-
this.onInput(
|
|
207
|
-
kind: "down",
|
|
208
|
-
x: location.x,
|
|
209
|
-
y: location.y,
|
|
210
|
-
button,
|
|
211
|
-
modifiers: modifierMask(event)
|
|
212
|
-
}));
|
|
213
|
+
this.onInput(this.inputEncoder.encodePointerDown(location, button, event));
|
|
213
214
|
event.preventDefault();
|
|
214
215
|
};
|
|
215
216
|
const handlePointerUp = (event) => {
|
|
@@ -217,39 +218,21 @@ var WebHostSceneRuntime = class {
|
|
|
217
218
|
this.terminalMount.releasePointerCapture?.(event.pointerId);
|
|
218
219
|
this.hasCapturedPointer = false;
|
|
219
220
|
if (!location) return;
|
|
220
|
-
this.
|
|
221
|
-
|
|
222
|
-
x: location.x,
|
|
223
|
-
y: location.y,
|
|
224
|
-
button: pointerButton(event.button) ?? this.activePointerButton,
|
|
225
|
-
modifiers: modifierMask(event)
|
|
226
|
-
}));
|
|
221
|
+
const button = this.inputEncoder.pointerButton(event.button) ?? this.activePointerButton;
|
|
222
|
+
this.onInput(this.inputEncoder.encodePointerUp(location, button, event));
|
|
227
223
|
event.preventDefault();
|
|
228
224
|
};
|
|
229
225
|
const handlePointerMove = (event) => {
|
|
230
226
|
const location = event.buttons && this.hasCapturedPointer ? this.rawCellLocation(event) : this.cellLocation(event);
|
|
231
227
|
if (!location) return;
|
|
232
|
-
this.onInput(
|
|
233
|
-
kind: event.buttons ? "dragged" : "moved",
|
|
234
|
-
x: location.x,
|
|
235
|
-
y: location.y,
|
|
236
|
-
button: this.activePointerButton,
|
|
237
|
-
modifiers: modifierMask(event)
|
|
238
|
-
}));
|
|
228
|
+
this.onInput(this.inputEncoder.encodePointerMove(location, this.activePointerButton, event));
|
|
239
229
|
};
|
|
240
230
|
const handleWheel = (event) => {
|
|
241
231
|
if (this.wheelMode === "passive") return;
|
|
242
232
|
const location = this.cellLocation(event);
|
|
243
233
|
if (!location) return;
|
|
244
|
-
if (this.wheelMode === "chain" && !this.
|
|
245
|
-
this.onInput(
|
|
246
|
-
kind: "scrolled",
|
|
247
|
-
x: location.x,
|
|
248
|
-
y: location.y,
|
|
249
|
-
deltaX: normalizedWheelDelta(event.deltaX),
|
|
250
|
-
deltaY: normalizedWheelDelta(event.deltaY),
|
|
251
|
-
modifiers: modifierMask(event)
|
|
252
|
-
}));
|
|
234
|
+
if (this.wheelMode === "chain" && !wheelTargetCanScroll(this.currentFrame?.scrollRegions, location, event.deltaX, event.deltaY)) return;
|
|
235
|
+
this.onInput(this.inputEncoder.encodeWheel(location, event));
|
|
253
236
|
event.preventDefault();
|
|
254
237
|
};
|
|
255
238
|
this.terminalMount.addEventListener("keydown", handleKeyDown);
|
|
@@ -313,53 +296,12 @@ var WebHostSceneRuntime = class {
|
|
|
313
296
|
this.cellHeight = Math.max(1, Math.round(this.currentStyle.fontSize * 1.35));
|
|
314
297
|
return;
|
|
315
298
|
}
|
|
316
|
-
context.font = this.
|
|
299
|
+
context.font = fontForStyle(this.currentStyle);
|
|
317
300
|
this.cellWidth = Math.max(1, Math.ceil(context.measureText("W").width));
|
|
318
301
|
this.cellHeight = Math.max(1, Math.ceil(this.currentStyle.fontSize * 1.35));
|
|
319
302
|
}
|
|
320
303
|
draw(damage) {
|
|
321
|
-
|
|
322
|
-
const context = canvas?.getContext("2d");
|
|
323
|
-
if (!canvas || !context) return;
|
|
324
|
-
const frame = this.currentFrame;
|
|
325
|
-
const dirtyRegion = frame ? this.dirtyRegionForDamage(damage, frame) : void 0;
|
|
326
|
-
if (dirtyRegion?.rects.length === 0) return;
|
|
327
|
-
const scale = globalThis.window?.devicePixelRatio || 1;
|
|
328
|
-
context.setTransform(scale, 0, 0, scale, 0, 0);
|
|
329
|
-
context.textBaseline = "alphabetic";
|
|
330
|
-
context.fillStyle = webTUITerminalBackgroundColor(this.currentStyle);
|
|
331
|
-
if (dirtyRegion) for (const rect of dirtyRegion.rects) {
|
|
332
|
-
context.clearRect(rect.x, rect.y, rect.width, rect.height);
|
|
333
|
-
context.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
context.clearRect(0, 0, canvas.width / scale, canvas.height / scale);
|
|
337
|
-
context.fillRect(0, 0, this.columns * this.cellWidth, this.rows * this.cellHeight);
|
|
338
|
-
}
|
|
339
|
-
if (!frame) return;
|
|
340
|
-
this.drawRows(context, frame, dirtyRegion);
|
|
341
|
-
this.drawImages(context, frame.images ?? [], dirtyRegion);
|
|
342
|
-
}
|
|
343
|
-
drawRows(context, frame, dirtyRegion) {
|
|
344
|
-
if (dirtyRegion) {
|
|
345
|
-
for (const [y, ranges] of dirtyRegion.rows) {
|
|
346
|
-
const row = frame.rows[y] ?? [];
|
|
347
|
-
this.drawRow(context, frame, row, y, ranges);
|
|
348
|
-
}
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
for (let y = 0; y < frame.rows.length; y += 1) {
|
|
352
|
-
const row = frame.rows[y] ?? [];
|
|
353
|
-
this.drawRow(context, frame, row, y);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
drawRow(context, frame, row, y, ranges) {
|
|
357
|
-
for (const cell of row) {
|
|
358
|
-
const [x, text, span, styleIndex] = cell;
|
|
359
|
-
if (ranges !== void 0 && !cellIntersectsRanges(x, span, ranges)) continue;
|
|
360
|
-
const style = frame.styles[styleIndex] ?? void 0;
|
|
361
|
-
this.drawCell(context, x, y, text, span, style);
|
|
362
|
-
}
|
|
304
|
+
this.painter.paint(this.surfaceMetrics(), this.currentFrame, damage);
|
|
363
305
|
}
|
|
364
306
|
syncAccessibilityTree() {
|
|
365
307
|
const tree = this.accessibilityTree;
|
|
@@ -369,292 +311,31 @@ var WebHostSceneRuntime = class {
|
|
|
369
311
|
cellHeight: this.cellHeight
|
|
370
312
|
}, this.currentFrame.accessibilityAnnouncements ?? [], { synchronizeFocus: this.synchronizeAccessibilityFocus });
|
|
371
313
|
}
|
|
372
|
-
|
|
373
|
-
for (const image of images) this.drawImage(context, image, dirtyRegion);
|
|
374
|
-
}
|
|
375
|
-
drawImage(context, image, dirtyRegion) {
|
|
376
|
-
const decodedImage = this.cachedImage(image);
|
|
377
|
-
if (!decodedImage) return;
|
|
378
|
-
const [boundsX, boundsY, boundsWidth, boundsHeight] = image.bounds;
|
|
379
|
-
const [clipX, clipY, clipWidth, clipHeight] = image.visibleBounds;
|
|
380
|
-
if (boundsWidth <= 0 || boundsHeight <= 0 || clipWidth <= 0 || clipHeight <= 0) return;
|
|
381
|
-
if (dirtyRegion && !dirtyRegionIntersectsCellRect(dirtyRegion, clipX, clipY, clipWidth, clipHeight)) return;
|
|
382
|
-
context.save();
|
|
383
|
-
context.beginPath();
|
|
384
|
-
context.rect(clipX * this.cellWidth, clipY * this.cellHeight, clipWidth * this.cellWidth, clipHeight * this.cellHeight);
|
|
385
|
-
context.clip();
|
|
386
|
-
context.drawImage(decodedImage, boundsX * this.cellWidth, boundsY * this.cellHeight, boundsWidth * this.cellWidth, boundsHeight * this.cellHeight);
|
|
387
|
-
context.restore();
|
|
388
|
-
}
|
|
389
|
-
cachedImage(image) {
|
|
390
|
-
const cached = this.imageCache.get(image.id);
|
|
391
|
-
if (cached?.image) return cached.image;
|
|
392
|
-
if (!cached?.promise && image.dataBase64) {
|
|
393
|
-
const promise = decodeImage(image.dataBase64, image.format);
|
|
394
|
-
this.imageCache.set(image.id, { promise });
|
|
395
|
-
promise.then((decodedImage) => {
|
|
396
|
-
if (this.imageCache.get(image.id)?.promise !== promise) return;
|
|
397
|
-
this.imageCache.set(image.id, { image: decodedImage });
|
|
398
|
-
this.draw();
|
|
399
|
-
}).catch(() => {
|
|
400
|
-
this.imageCache.delete(image.id);
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
drawCell(context, x, y, text, span, style) {
|
|
405
|
-
const rectX = x * this.cellWidth;
|
|
406
|
-
const rectY = y * this.cellHeight;
|
|
407
|
-
const width = Math.max(1, span) * this.cellWidth;
|
|
408
|
-
const background = resolvedBackground(style, this.currentStyle);
|
|
409
|
-
const foreground = resolvedForeground(style, this.currentStyle);
|
|
410
|
-
const opacity = style?.opacity ?? 1;
|
|
411
|
-
if (background) {
|
|
412
|
-
context.globalAlpha = opacity;
|
|
413
|
-
context.fillStyle = background;
|
|
414
|
-
context.fillRect(rectX, rectY, width, this.cellHeight);
|
|
415
|
-
}
|
|
416
|
-
if (text !== " ") {
|
|
417
|
-
context.globalAlpha = opacity;
|
|
418
|
-
context.fillStyle = foreground;
|
|
419
|
-
context.strokeStyle = foreground;
|
|
420
|
-
if (!canRenderBoxDrawing(text) || !drawBoxDrawing(context, text, {
|
|
421
|
-
x: rectX,
|
|
422
|
-
y: rectY,
|
|
423
|
-
width,
|
|
424
|
-
height: this.cellHeight
|
|
425
|
-
})) {
|
|
426
|
-
context.font = this.fontForStyle(style);
|
|
427
|
-
context.fillText(text, rectX, rectY + Math.floor((this.cellHeight + this.currentStyle.fontSize) / 2) - 2);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
this.drawTextLine(context, rectX, rectY, width, style?.underline, "underline", foreground);
|
|
431
|
-
this.drawTextLine(context, rectX, rectY, width, style?.strikethrough, "strike", foreground);
|
|
432
|
-
context.globalAlpha = 1;
|
|
433
|
-
}
|
|
434
|
-
dirtyRegionForDamage(damage, frame) {
|
|
435
|
-
if (!damage || damage.requiresFullTextRepaint || damage.requiresFullGraphicsReplay) return;
|
|
436
|
-
const rects = [];
|
|
437
|
-
const rows = /* @__PURE__ */ new Map();
|
|
438
|
-
for (const [row, ranges] of damage.textRows) {
|
|
439
|
-
if (row < 0 || row >= frame.height) continue;
|
|
440
|
-
if (ranges.length === 0) {
|
|
441
|
-
rects.push(this.cellRect(0, row, frame.width));
|
|
442
|
-
rows.set(row, "full");
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
445
|
-
const rowRanges = rows.get(row) === "full" ? [] : [...rows.get(row) ?? []];
|
|
446
|
-
for (const [start, end] of ranges) {
|
|
447
|
-
const lowerBound = Math.max(0, Math.min(frame.width, Math.floor(start)));
|
|
448
|
-
const upperBound = Math.max(lowerBound, Math.min(frame.width, Math.ceil(end)));
|
|
449
|
-
if (lowerBound >= upperBound) continue;
|
|
450
|
-
rects.push(this.cellRect(lowerBound, row, upperBound - lowerBound));
|
|
451
|
-
rowRanges.push({
|
|
452
|
-
start: lowerBound,
|
|
453
|
-
end: upperBound
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
if (rows.get(row) !== "full" && rowRanges.length > 0) rows.set(row, normalizeCellRanges(rowRanges));
|
|
457
|
-
}
|
|
314
|
+
surfaceMetrics() {
|
|
458
315
|
return {
|
|
459
|
-
|
|
460
|
-
rows
|
|
316
|
+
columns: this.columns,
|
|
317
|
+
rows: this.rows,
|
|
318
|
+
cellWidth: this.cellWidth,
|
|
319
|
+
cellHeight: this.cellHeight,
|
|
320
|
+
style: this.currentStyle
|
|
461
321
|
};
|
|
462
322
|
}
|
|
463
|
-
|
|
323
|
+
pointerMetrics() {
|
|
464
324
|
return {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
325
|
+
rect: this.canvas?.getBoundingClientRect?.() ?? this.terminalMount.getBoundingClientRect?.(),
|
|
326
|
+
cellWidth: this.cellWidth,
|
|
327
|
+
cellHeight: this.cellHeight,
|
|
328
|
+
columns: this.columns,
|
|
329
|
+
rows: this.rows
|
|
469
330
|
};
|
|
470
331
|
}
|
|
471
|
-
drawTextLine(context, x, y, width, line, placement, fallbackColor) {
|
|
472
|
-
if (!line) return;
|
|
473
|
-
context.strokeStyle = line.color ?? fallbackColor;
|
|
474
|
-
context.lineWidth = line.pattern === "double" ? 2 : 1;
|
|
475
|
-
if (line.pattern === "dot") context.setLineDash([1, 3]);
|
|
476
|
-
else if (line.pattern === "dash") context.setLineDash([4, 3]);
|
|
477
|
-
else context.setLineDash([]);
|
|
478
|
-
const lineY = placement === "underline" ? y + this.cellHeight - 2 : y + Math.floor(this.cellHeight / 2);
|
|
479
|
-
context.beginPath();
|
|
480
|
-
context.moveTo(x, lineY);
|
|
481
|
-
context.lineTo(x + width, lineY);
|
|
482
|
-
context.stroke();
|
|
483
|
-
context.setLineDash([]);
|
|
484
|
-
}
|
|
485
|
-
fontForStyle(style) {
|
|
486
|
-
const emphasis = style?.em ?? 0;
|
|
487
|
-
return `${(emphasis & 2) !== 0 ? "italic " : ""}${(emphasis & 1) !== 0 ? "700 " : ""}${this.currentStyle.fontSize}px ${this.currentStyle.fontFamily}`;
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* Whether any scrollable region under `location` can still scroll in the
|
|
491
|
-
* wheel's direction. Mirrors the Swift host's scroll hit-test: a region
|
|
492
|
-
* qualifies when its viewport contains the cell AND it has remaining headroom
|
|
493
|
-
* in the delta's direction. Used by "chain" wheel mode to decide capture vs.
|
|
494
|
-
* fall-through. With no published `scrollRegions`, nothing can scroll, so the
|
|
495
|
-
* wheel chains to the page (a scene with no ScrollView stays fully passive).
|
|
496
|
-
*/
|
|
497
|
-
wheelTargetCanScroll(location, deltaX, deltaY) {
|
|
498
|
-
const regions = this.currentFrame?.scrollRegions;
|
|
499
|
-
if (!regions || regions.length === 0) return false;
|
|
500
|
-
const cellX = Math.floor(location.x);
|
|
501
|
-
const cellY = Math.floor(location.y);
|
|
502
|
-
for (const region of regions) {
|
|
503
|
-
const [rx, ry, rw, rh] = region.rect;
|
|
504
|
-
if (cellX < rx || cellY < ry || cellX >= rx + rw || cellY >= ry + rh) continue;
|
|
505
|
-
if (regionCanScrollInDirection(region, deltaX, deltaY)) return true;
|
|
506
|
-
}
|
|
507
|
-
return false;
|
|
508
|
-
}
|
|
509
332
|
cellLocation(event) {
|
|
510
|
-
|
|
511
|
-
if (!location) return;
|
|
512
|
-
const cellX = Math.floor(location.x);
|
|
513
|
-
const cellY = Math.floor(location.y);
|
|
514
|
-
if (cellX < 0 || cellY < 0 || cellX >= this.columns || cellY >= this.rows) return;
|
|
515
|
-
return location;
|
|
333
|
+
return cellLocationForEvent(event, this.pointerMetrics());
|
|
516
334
|
}
|
|
517
335
|
rawCellLocation(event) {
|
|
518
|
-
|
|
519
|
-
if (!rect) return;
|
|
520
|
-
return {
|
|
521
|
-
x: (event.clientX - rect.left) / this.cellWidth,
|
|
522
|
-
y: (event.clientY - rect.top) / this.cellHeight
|
|
523
|
-
};
|
|
336
|
+
return rawCellLocationForEvent(event, this.pointerMetrics());
|
|
524
337
|
}
|
|
525
338
|
};
|
|
526
|
-
async function decodeImage(dataBase64, format) {
|
|
527
|
-
const bytes = decodeBase64Bytes(dataBase64);
|
|
528
|
-
const blob = new Blob([bytes], { type: `image/${format}` });
|
|
529
|
-
if (typeof createImageBitmap === "function") return createImageBitmap(blob);
|
|
530
|
-
return new Promise((resolve, reject) => {
|
|
531
|
-
const image = new Image();
|
|
532
|
-
const url = URL.createObjectURL(blob);
|
|
533
|
-
image.onload = () => {
|
|
534
|
-
URL.revokeObjectURL(url);
|
|
535
|
-
resolve(image);
|
|
536
|
-
};
|
|
537
|
-
image.onerror = () => {
|
|
538
|
-
URL.revokeObjectURL(url);
|
|
539
|
-
reject(/* @__PURE__ */ new Error(`Failed to decode ${format} image`));
|
|
540
|
-
};
|
|
541
|
-
image.src = url;
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
function decodeBase64Bytes(value) {
|
|
545
|
-
if (typeof atob === "function") {
|
|
546
|
-
const binary = atob(value);
|
|
547
|
-
const bytes = new Uint8Array(binary.length);
|
|
548
|
-
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
549
|
-
return bytes;
|
|
550
|
-
}
|
|
551
|
-
return new Uint8Array(Buffer.from(value, "base64"));
|
|
552
|
-
}
|
|
553
|
-
function keyInputFromKeyboardEvent(event) {
|
|
554
|
-
switch (event.key) {
|
|
555
|
-
case "Enter": return { key: "return" };
|
|
556
|
-
case " ": return { key: "space" };
|
|
557
|
-
case "Tab": return { key: "tab" };
|
|
558
|
-
case "ArrowLeft": return { key: "arrowLeft" };
|
|
559
|
-
case "ArrowRight": return { key: "arrowRight" };
|
|
560
|
-
case "ArrowUp": return { key: "arrowUp" };
|
|
561
|
-
case "ArrowDown": return { key: "arrowDown" };
|
|
562
|
-
case "Backspace": return { key: "backspace" };
|
|
563
|
-
case "Escape": return { key: "escape" };
|
|
564
|
-
case "Home": return { key: "home" };
|
|
565
|
-
case "End": return { key: "end" };
|
|
566
|
-
default: {
|
|
567
|
-
const characters = Array.from(event.key);
|
|
568
|
-
if (characters.length !== 1) return;
|
|
569
|
-
return {
|
|
570
|
-
key: "character",
|
|
571
|
-
character: characters[0]
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
function pointerButton(button) {
|
|
577
|
-
switch (button) {
|
|
578
|
-
case 1: return "middle";
|
|
579
|
-
case 2: return "secondary";
|
|
580
|
-
default: return "primary";
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
function modifierMask(event) {
|
|
584
|
-
let mask = 0;
|
|
585
|
-
if (event.shiftKey) mask |= 1;
|
|
586
|
-
if (event.altKey) mask |= 2;
|
|
587
|
-
if (event.ctrlKey) mask |= 4;
|
|
588
|
-
return mask;
|
|
589
|
-
}
|
|
590
|
-
function normalizedWheelDelta(delta) {
|
|
591
|
-
if (delta > 0) return 1;
|
|
592
|
-
if (delta < 0) return -1;
|
|
593
|
-
return 0;
|
|
594
|
-
}
|
|
595
|
-
/**
|
|
596
|
-
* Whether a published scroll region has remaining headroom in the wheel's
|
|
597
|
-
* direction, recomputing the per-direction extent from offset/content/viewport.
|
|
598
|
-
* Mirrors SwiftTUI's clamp (`min(max(0, offset), max(0, content - viewport))`)
|
|
599
|
-
* so the host and the app agree on "at edge". Wheel sign convention matches the
|
|
600
|
-
* app: `deltaY > 0` scrolls down (offset grows toward the content bottom).
|
|
601
|
-
* Diagonal wheels qualify if either axis has headroom.
|
|
602
|
-
*/
|
|
603
|
-
function regionCanScrollInDirection(region, deltaX, deltaY) {
|
|
604
|
-
const [, , viewportWidth, viewportHeight] = region.rect;
|
|
605
|
-
const [offsetX, offsetY] = region.offset;
|
|
606
|
-
const [contentWidth, contentHeight] = region.content;
|
|
607
|
-
const maxX = Math.max(0, contentWidth - viewportWidth);
|
|
608
|
-
const maxY = Math.max(0, contentHeight - viewportHeight);
|
|
609
|
-
const clampedX = Math.min(Math.max(0, offsetX), maxX);
|
|
610
|
-
const clampedY = Math.min(Math.max(0, offsetY), maxY);
|
|
611
|
-
if (deltaY > 0 && clampedY < maxY) return true;
|
|
612
|
-
if (deltaY < 0 && clampedY > 0) return true;
|
|
613
|
-
if (deltaX > 0 && clampedX < maxX) return true;
|
|
614
|
-
if (deltaX < 0 && clampedX > 0) return true;
|
|
615
|
-
return false;
|
|
616
|
-
}
|
|
617
|
-
function normalizeCellRanges(ranges) {
|
|
618
|
-
const sorted = ranges.filter((range) => range.end > range.start).sort((lhs, rhs) => lhs.start - rhs.start || lhs.end - rhs.end);
|
|
619
|
-
const normalized = [];
|
|
620
|
-
for (const range of sorted) {
|
|
621
|
-
const previous = normalized[normalized.length - 1];
|
|
622
|
-
if (previous && range.start <= previous.end) {
|
|
623
|
-
previous.end = Math.max(previous.end, range.end);
|
|
624
|
-
continue;
|
|
625
|
-
}
|
|
626
|
-
normalized.push({ ...range });
|
|
627
|
-
}
|
|
628
|
-
return normalized;
|
|
629
|
-
}
|
|
630
|
-
function cellIntersectsRanges(x, span, ranges) {
|
|
631
|
-
if (ranges === "full") return true;
|
|
632
|
-
const start = Math.floor(x);
|
|
633
|
-
const end = start + Math.max(1, Math.ceil(span));
|
|
634
|
-
return ranges.some((range) => start < range.end && end > range.start);
|
|
635
|
-
}
|
|
636
|
-
function dirtyRegionIntersectsCellRect(region, x, y, width, height) {
|
|
637
|
-
const startRow = Math.max(0, Math.floor(y));
|
|
638
|
-
const endRow = Math.max(startRow, Math.ceil(y + height));
|
|
639
|
-
const rectRange = {
|
|
640
|
-
start: Math.floor(x),
|
|
641
|
-
end: Math.floor(x) + Math.max(1, Math.ceil(width))
|
|
642
|
-
};
|
|
643
|
-
for (let row = startRow; row < endRow; row += 1) {
|
|
644
|
-
const ranges = region.rows.get(row);
|
|
645
|
-
if (!ranges) continue;
|
|
646
|
-
if (cellIntersectsRanges(rectRange.start, rectRange.end - rectRange.start, ranges)) return true;
|
|
647
|
-
}
|
|
648
|
-
return false;
|
|
649
|
-
}
|
|
650
|
-
function resolvedForeground(style, terminalStyle) {
|
|
651
|
-
if ((style?.em ?? 0) & 16) return style?.bg ?? terminalStyle.theme.background;
|
|
652
|
-
return style?.fg ?? terminalStyle.theme.foreground;
|
|
653
|
-
}
|
|
654
|
-
function resolvedBackground(style, terminalStyle) {
|
|
655
|
-
if ((style?.em ?? 0) & 16) return style?.fg ?? terminalStyle.theme.foreground;
|
|
656
|
-
return style?.bg;
|
|
657
|
-
}
|
|
658
339
|
//#endregion
|
|
659
340
|
export { WebHostSceneRuntime };
|
|
660
341
|
|