@tn3270/gui 0.0.0-stage → 0.1.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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@tn3270/gui",
3
+ "version": "0.1.0",
4
+ "description": "An Electron TN3270 emulator with a canvas renderer that blits glyphs from an atlas baked out of x3270's own 3270 bitmap font.",
5
+ "license": "MIT",
6
+ "author": "Adam Thornton <athornton@gmail.com>",
7
+ "type": "module",
8
+ "main": "main.js"
9
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+
3
+ // packages/gui/dist/preload.cjs
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ var electron_1 = require("electron");
6
+ electron_1.contextBridge.exposeInMainWorld("tn3270", {
7
+ /** Receive the glyph atlas once: geometry, coverage bytes and the blank-column set. */
8
+ onAtlas: (fn) => {
9
+ electron_1.ipcRenderer.on("atlas", (_e, atlas) => {
10
+ fn(atlas);
11
+ });
12
+ },
13
+ /**
14
+ * Receive a finished draw list.
15
+ *
16
+ * The LIST, not the snapshot: `drawList` needs core's palette and code page, and a
17
+ * browser cannot resolve a bare specifier like `@tn3270/core` without a bundler. Doing
18
+ * the work in main means the renderer imports nothing but its own relative modules --
19
+ * which is also less for it to know, and it already owns no protocol state.
20
+ */
21
+ onFrame: (fn) => {
22
+ electron_1.ipcRenderer.on("frame", (_e, frame) => {
23
+ fn(frame);
24
+ });
25
+ },
26
+ /** Receive a message to show instead of a screen (connect and TLS failures). */
27
+ onError: (fn) => {
28
+ electron_1.ipcRenderer.on("error-message", (_e, message) => {
29
+ fn(message);
30
+ });
31
+ },
32
+ /** Send one named 3270 action up to the session. */
33
+ sendAction: (action) => {
34
+ electron_1.ipcRenderer.send("action", action);
35
+ }
36
+ });
37
+ //# sourceMappingURL=preload.cjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/preload.cts"],
4
+ "sourcesContent": ["import { contextBridge, ipcRenderer } from 'electron';\n\n/**\n * A `.cts` FILE ON PURPOSE. Electron preload scripts are CommonJS -- an ESM preload fails\n * with \"Cannot use import statement outside a module\" and the bridge silently never\n * appears, leaving a blank window and no error in the renderer. TypeScript compiles `.cts`\n * to `.cjs` with CommonJS semantics, so this stays typechecked instead of becoming a\n * hand-written JavaScript file.\n */\n\n/**\n * The ONLY bridge between the protocol and the canvas: frames down, actions up.\n *\n * `contextIsolation` is on and `nodeIntegration` off, so the renderer cannot reach a socket\n * even if it tried. That is not ceremony -- the renderer parses nothing and owns no protocol\n * state, so a compromise there costs a repaint. Widening this surface is what would change\n * that, which is why it is two functions and not an object graph.\n */\ncontextBridge.exposeInMainWorld('tn3270', {\n /** Receive the glyph atlas once: geometry, coverage bytes and the blank-column set. */\n onAtlas: (fn: (atlas: unknown) => void) => {\n ipcRenderer.on('atlas', (_e, atlas) => { fn(atlas); });\n },\n /**\n * Receive a finished draw list.\n *\n * The LIST, not the snapshot: `drawList` needs core's palette and code page, and a\n * browser cannot resolve a bare specifier like `@tn3270/core` without a bundler. Doing\n * the work in main means the renderer imports nothing but its own relative modules --\n * which is also less for it to know, and it already owns no protocol state.\n */\n onFrame: (fn: (frame: unknown) => void) => {\n ipcRenderer.on('frame', (_e, frame) => { fn(frame); });\n },\n /** Receive a message to show instead of a screen (connect and TLS failures). */\n onError: (fn: (message: string) => void) => {\n ipcRenderer.on('error-message', (_e, message: string) => { fn(message); });\n },\n /** Send one named 3270 action up to the session. */\n sendAction: (action: unknown) => { ipcRenderer.send('action', action); },\n});\n"],
5
+ "mappings": ";;;;AAAA,IAAA,aAAA,QAAA,UAAA;AAkBA,WAAA,cAAc,kBAAkB,UAAU;;EAExC,SAAS,CAAC,OAAgC;AACxC,eAAA,YAAY,GAAG,SAAS,CAAC,IAAI,UAAS;AAAG,SAAG,KAAK;IAAG,CAAC;EACvD;;;;;;;;;EASA,SAAS,CAAC,OAAgC;AACxC,eAAA,YAAY,GAAG,SAAS,CAAC,IAAI,UAAS;AAAG,SAAG,KAAK;IAAG,CAAC;EACvD;;EAEA,SAAS,CAAC,OAAiC;AACzC,eAAA,YAAY,GAAG,iBAAiB,CAAC,IAAI,YAAmB;AAAG,SAAG,OAAO;IAAG,CAAC;EAC3E;;EAEA,YAAY,CAAC,WAAmB;AAAG,eAAA,YAAY,KAAK,UAAU,MAAM;EAAG;CACxE;",
6
+ "names": []
7
+ }
@@ -0,0 +1,559 @@
1
+ // packages/canvas/dist/keys.js
2
+ function layoutCharacter(e) {
3
+ if (e.getModifierState?.("AltGraph") !== true)
4
+ return null;
5
+ if ([...e.key].length === 1)
6
+ return { kind: "type", text: e.key };
7
+ return null;
8
+ }
9
+ var NAMED = Object.freeze({
10
+ Enter: { kind: "enter" },
11
+ ArrowLeft: { kind: "left" },
12
+ ArrowRight: { kind: "right" },
13
+ ArrowUp: { kind: "up" },
14
+ ArrowDown: { kind: "down" },
15
+ Home: { kind: "home" },
16
+ Backspace: { kind: "backspace" },
17
+ Delete: { kind: "delete" },
18
+ // vt220 Select/End bound to EraseEOF, following c3270 -- a CHOICE, not a derivation.
19
+ End: { kind: "eraseEOF" },
20
+ // x3270's Toggle(insertMode), fb-x3270:210.
21
+ Insert: { kind: "toggleInsert" }
22
+ });
23
+ var MODIFIERS = /* @__PURE__ */ new Set([
24
+ "Shift",
25
+ "Control",
26
+ "Alt",
27
+ "Meta",
28
+ "CapsLock",
29
+ "AltGraph",
30
+ "NumLock",
31
+ "ScrollLock"
32
+ ]);
33
+ var CTRL = Object.freeze({
34
+ c: { kind: "clear" },
35
+ r: { kind: "reset" },
36
+ u: { kind: "eraseInput" },
37
+ "]": { kind: "quit" },
38
+ a: { kind: "attn" },
39
+ d: { kind: "dup" },
40
+ f: { kind: "fieldMark" },
41
+ k: { kind: "toggleKeypad" },
42
+ // The full 122-key keyboard, 2026-10-22. Free in both mappers -- the taken chords are
43
+ // C R U ] A D F K T -- and claiming it also suppresses Firefox's bookmarks sidebar, which is
44
+ // correct: `renderer.ts` calls `preventDefault` only for keys we claimed, so claiming it is
45
+ // what makes it ours. Intercepted CLIENT-SIDE in both DOM front ends, exactly as `toggleKeypad`
46
+ // is, and the gateway returns before `applyAction` for it -- see `web/src/main.ts`.
47
+ b: { kind: "toggleKeyboard" },
48
+ /**
49
+ * Ctrl-T, the transfer form -- `BINDING_INTENT`'s own spelling, now that the GUI has a dialog.
50
+ *
51
+ * THE GUI HAD THIS CHORD DEAD WHILE THE WINDOW WORKED, which is how this was found: a real
52
+ * Electron run with `TN3270_GUI_KEYS=Ctrl+T` printed `keys: sent Ctrl+T` and NO `action:` line
53
+ * at all, where a click on the `Xfer` keypad button opened the window fine. The keypad route
54
+ * went through `KEYPAD_KEYS` and never through this table, so the one that was missing was also
55
+ * the one no harness drove.
56
+ *
57
+ * ACCEPTED BY THE WEB FRONT END TOO, since this mapper is shared, and that is safe rather than
58
+ * merely tolerable: `protocol.ts:129-131` REJECTS the kind at decode with a per-client `error`
59
+ * frame, and `web/src/main.ts:151-154` answers a decode failure on that one socket without
60
+ * touching the session or the process. The browser could already produce this kind from the
61
+ * `Xfer` button it draws today, so this adds a second route to an answer that already exists --
62
+ * no new failure mode, and the refusal names its reason. When stage 4 gives the gateway a
63
+ * transfer path that rejection becomes an interception, and this entry needs no change.
64
+ */
65
+ t: { kind: "transferForm" }
66
+ });
67
+ var PA_CODES = Object.freeze({
68
+ Digit1: { kind: "pa", n: 1 },
69
+ Digit2: { kind: "pa", n: 2 },
70
+ Digit3: { kind: "pa", n: 3 }
71
+ });
72
+ function actionForKey(e) {
73
+ if (MODIFIERS.has(e.key))
74
+ return null;
75
+ if (e.shiftKey && !e.altKey && e.ctrlKey !== e.metaKey && e.code === "KeyC") {
76
+ return { kind: "requestCopy" };
77
+ }
78
+ if (e.ctrlKey && !e.altKey && !e.metaKey) {
79
+ return CTRL[e.key.toLowerCase()] ?? null;
80
+ }
81
+ if (e.altKey && !e.ctrlKey && !e.metaKey) {
82
+ if (e.code === "KeyK")
83
+ return { kind: "toggleKeypad" };
84
+ if (e.code === "KeyM")
85
+ return { kind: "geometryMenu" };
86
+ return PA_CODES[e.code] ?? layoutCharacter(e);
87
+ }
88
+ if (e.metaKey || e.altKey)
89
+ return layoutCharacter(e);
90
+ if (e.key === "Tab")
91
+ return e.shiftKey ? { kind: "backTab" } : { kind: "tab" };
92
+ const fn = /^F(\d{1,2})$/.exec(e.key);
93
+ if (fn !== null) {
94
+ const n = Number(fn[1]);
95
+ if (n < 1 || n > 12)
96
+ return null;
97
+ return { kind: "pf", n: e.shiftKey ? n + 12 : n };
98
+ }
99
+ const named = NAMED[e.key];
100
+ if (named !== void 0)
101
+ return named;
102
+ if ([...e.key].length === 1)
103
+ return { kind: "type", text: e.key };
104
+ return null;
105
+ }
106
+
107
+ // packages/canvas/dist/forwardKeys.js
108
+ function isEditableTarget(target) {
109
+ const tag = typeof target?.tagName === "string" ? target.tagName : "";
110
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || target?.isContentEditable === true;
111
+ }
112
+ var PASTE_HOSTS = /* @__PURE__ */ new Set(["http:", "https:"]);
113
+ function forwardPaste(deps) {
114
+ if (!PASTE_HOSTS.has(deps.protocol))
115
+ return;
116
+ deps.on("paste", (e) => {
117
+ if (isEditableTarget(e.target))
118
+ return;
119
+ const text = e.clipboardData?.getData("text/plain") ?? "";
120
+ if (text === "")
121
+ return;
122
+ e.preventDefault();
123
+ deps.clearSelection();
124
+ deps.sendAction({ kind: "paste", text });
125
+ });
126
+ }
127
+ function forwardCopy(deps) {
128
+ if (!PASTE_HOSTS.has(deps.protocol))
129
+ return;
130
+ deps.on("copy", (e) => {
131
+ if (isEditableTarget(e.target))
132
+ return;
133
+ deps.onHandled();
134
+ const text = deps.cached();
135
+ if (text !== void 0 && text !== "" && e.clipboardData) {
136
+ e.clipboardData.setData("text/plain", text);
137
+ e.preventDefault();
138
+ deps.onCopied?.();
139
+ return;
140
+ }
141
+ deps.copySelection();
142
+ });
143
+ }
144
+
145
+ // packages/canvas/dist/blit.js
146
+ var rgbCss = ([r, g, b]) => `rgb(${r},${g},${b})`;
147
+ var tintKey = ([r, g, b]) => `${r},${g},${b}`;
148
+ function bestScale(list, within) {
149
+ const byWidth = Math.floor(within.width / list.width);
150
+ const byHeight = Math.floor(within.height / list.height);
151
+ return Math.max(1, Math.min(byWidth, byHeight));
152
+ }
153
+ function center(list, within, scale) {
154
+ return {
155
+ x: Math.max(0, Math.floor((within.width - list.width * scale) / 2)),
156
+ y: Math.max(0, Math.floor((within.height - list.height * scale) / 2))
157
+ };
158
+ }
159
+ function blit(ctx2, list, opts) {
160
+ const { atlas: atlas2, scale, offsetX, offsetY } = opts;
161
+ ctx2.imageSmoothingEnabled = false;
162
+ const w = atlas2.cellWidth * scale;
163
+ const h = atlas2.cellHeight * scale;
164
+ for (const cell of list.cells) {
165
+ const dx = offsetX + cell.x * scale;
166
+ const dy = offsetY + cell.y * scale;
167
+ ctx2.fillStyle = rgbCss(cell.bg);
168
+ ctx2.fillRect(dx, dy, w, h);
169
+ if (!opts.blank.has(cell.glyph)) {
170
+ const image = opts.tinted(cell.fg);
171
+ if (image !== void 0) {
172
+ ctx2.drawImage(image, cell.glyph * atlas2.cellWidth, 0, atlas2.cellWidth, atlas2.cellHeight, dx, dy, w, h);
173
+ }
174
+ }
175
+ if (cell.underline) {
176
+ ctx2.fillStyle = rgbCss(cell.fg);
177
+ ctx2.fillRect(dx, dy + h - scale, w, scale);
178
+ }
179
+ if (cell.cursor) {
180
+ ctx2.fillStyle = rgbCss(cell.fg);
181
+ ctx2.fillRect(dx, dy + h - Math.max(scale, Math.floor(h / 4)), w, Math.max(scale, Math.floor(h / 4)));
182
+ }
183
+ }
184
+ }
185
+
186
+ // packages/canvas/dist/selection.js
187
+ function normalizeRect(anchor2, focus2) {
188
+ return {
189
+ top: Math.min(anchor2.row, focus2.row),
190
+ left: Math.min(anchor2.col, focus2.col),
191
+ bottom: Math.max(anchor2.row, focus2.row),
192
+ right: Math.max(anchor2.col, focus2.col)
193
+ };
194
+ }
195
+ var DRAG_THRESHOLD_PX = 4;
196
+ function movedPastThreshold(press, now) {
197
+ return Math.abs(now.x - press.x) >= DRAG_THRESHOLD_PX || Math.abs(now.y - press.y) >= DRAG_THRESHOLD_PX;
198
+ }
199
+
200
+ // packages/canvas/dist/copyCache.js
201
+ function createCopyCache() {
202
+ let gen = 0;
203
+ let asked;
204
+ let text;
205
+ const same = (a, b) => a.top === b.top && a.left === b.left && a.bottom === b.bottom && a.right === b.right;
206
+ return {
207
+ begin(rect) {
208
+ gen += 1;
209
+ asked = { ...rect };
210
+ text = void 0;
211
+ return gen;
212
+ },
213
+ accept(g, t) {
214
+ if (g !== gen || t === "")
215
+ return;
216
+ text = t;
217
+ },
218
+ invalidate() {
219
+ gen += 1;
220
+ asked = void 0;
221
+ text = void 0;
222
+ },
223
+ take(rect) {
224
+ if (asked === void 0 || text === void 0 || !same(asked, rect))
225
+ return void 0;
226
+ return text;
227
+ }
228
+ };
229
+ }
230
+
231
+ // packages/canvas/dist/renderer.js
232
+ var canvas = document.querySelector("#screen");
233
+ if (canvas === null)
234
+ throw new Error("no #screen canvas in the document");
235
+ var real = canvas.getContext("2d");
236
+ if (real === null)
237
+ throw new Error("no 2D context: the window cannot draw");
238
+ var ctx = real;
239
+ var atlas;
240
+ var blank = /* @__PURE__ */ new Set();
241
+ var last;
242
+ var anchor = null;
243
+ var focus = null;
244
+ var dragging = false;
245
+ var pressAt = null;
246
+ var moved = false;
247
+ var copyCache = createCopyCache();
248
+ var copyHandled = false;
249
+ function clearSelection() {
250
+ anchor = null;
251
+ focus = null;
252
+ dragging = false;
253
+ pressAt = null;
254
+ moved = false;
255
+ copyCache.invalidate();
256
+ }
257
+ function selectedRect() {
258
+ if (anchor === null || focus === null || !moved)
259
+ return null;
260
+ return normalizeRect(anchor, focus);
261
+ }
262
+ var errored = false;
263
+ var noticeOnly = false;
264
+ var tints = /* @__PURE__ */ new Map();
265
+ var building = /* @__PURE__ */ new Set();
266
+ function tintOf(color) {
267
+ if (atlas === void 0)
268
+ return void 0;
269
+ const key = tintKey(color);
270
+ const got = tints.get(key);
271
+ if (got !== void 0)
272
+ return got;
273
+ if (building.has(key))
274
+ return void 0;
275
+ building.add(key);
276
+ const w = atlas.geometry.cols * atlas.geometry.cellWidth;
277
+ const h = atlas.geometry.cellHeight;
278
+ const data = new Uint8ClampedArray(w * h * 4);
279
+ for (let i = 0; i < atlas.coverage.length; i++) {
280
+ data[i * 4] = color[0];
281
+ data[i * 4 + 1] = color[1];
282
+ data[i * 4 + 2] = color[2];
283
+ data[i * 4 + 3] = atlas.coverage[i];
284
+ }
285
+ void createImageBitmap(new ImageData(data, w, h)).then((bmp) => {
286
+ tints.set(key, bmp);
287
+ building.delete(key);
288
+ if (last !== void 0)
289
+ paint(last);
290
+ });
291
+ return void 0;
292
+ }
293
+ function paint(list) {
294
+ last = list;
295
+ if (atlas === void 0 || canvas === null)
296
+ return;
297
+ const within = { width: window.innerWidth, height: window.innerHeight };
298
+ const scale = bestScale(list, within);
299
+ const at = center(list, within, scale);
300
+ canvas.width = Math.max(within.width, list.width * scale);
301
+ canvas.height = Math.max(within.height, list.height * scale);
302
+ ctx.fillStyle = "#000";
303
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
304
+ const options = {
305
+ atlas: atlas.geometry,
306
+ scale,
307
+ offsetX: at.x,
308
+ offsetY: at.y,
309
+ blank,
310
+ // undefined skips the glyph this frame; tintOf repaints when the bitmap is ready.
311
+ tinted: (color) => tintOf(color)
312
+ };
313
+ blit(ctx, list, options);
314
+ if (list.oia !== void 0) {
315
+ blit(ctx, { cells: list.oia.cells, width: list.width, height: list.height }, options);
316
+ }
317
+ const rect = selectedRect();
318
+ if (rect !== null && atlas !== void 0) {
319
+ const g = atlas.geometry;
320
+ ctx.save();
321
+ ctx.globalCompositeOperation = "difference";
322
+ ctx.fillStyle = "#ffffff";
323
+ ctx.fillRect(at.x + rect.left * g.cellWidth * scale, at.y + rect.top * g.cellHeight * scale, (rect.right - rect.left + 1) * g.cellWidth * scale, (rect.bottom - rect.top + 1) * g.cellHeight * scale);
324
+ ctx.restore();
325
+ }
326
+ }
327
+ window.tn3270.onAtlas((message) => {
328
+ atlas = message;
329
+ blank = new Set(message.blank);
330
+ if (last !== void 0) {
331
+ errored = false;
332
+ noticeOnly = false;
333
+ paint(last);
334
+ }
335
+ });
336
+ window.tn3270.onFrame((list) => {
337
+ errored = false;
338
+ noticeOnly = false;
339
+ clearSelection();
340
+ paint(list);
341
+ });
342
+ function paintMessage(message) {
343
+ if (canvas === null || real === null)
344
+ return;
345
+ errored = true;
346
+ canvas.width = window.innerWidth;
347
+ canvas.height = window.innerHeight;
348
+ real.fillStyle = "#000";
349
+ real.fillRect(0, 0, canvas.width, canvas.height);
350
+ real.fillStyle = "#ff5555";
351
+ real.font = "15px monospace";
352
+ let y = 40;
353
+ for (const line of wrap(message, Math.max(20, Math.floor(canvas.width / 9)))) {
354
+ real.fillText(line, 20, y);
355
+ y += 21;
356
+ }
357
+ }
358
+ window.tn3270.onError((message) => {
359
+ noticeOnly = false;
360
+ clearSelection();
361
+ paintMessage(message);
362
+ });
363
+ window.tn3270.onCopyFailed?.((message) => {
364
+ if (errored && !noticeOnly)
365
+ return;
366
+ noticeOnly = true;
367
+ paintMessage(message);
368
+ });
369
+ window.addEventListener("keydown", (e) => {
370
+ if (isEditableTarget(e.target)) {
371
+ return;
372
+ }
373
+ const action = actionForKey(e);
374
+ if (action === null)
375
+ return;
376
+ e.preventDefault();
377
+ if (action.kind === "requestCopy") {
378
+ copyByChord();
379
+ return;
380
+ }
381
+ clearSelection();
382
+ window.tn3270.sendAction(action);
383
+ });
384
+ forwardPaste({
385
+ on: (type, handler) => {
386
+ window.addEventListener(type, handler);
387
+ },
388
+ sendAction: (action) => {
389
+ window.tn3270.sendAction(action);
390
+ },
391
+ protocol: location.protocol,
392
+ clearSelection
393
+ });
394
+ forwardCopy({
395
+ on: (type, handler) => {
396
+ window.addEventListener(type, handler);
397
+ },
398
+ protocol: location.protocol,
399
+ cached: cachedSelection,
400
+ copySelection,
401
+ onHandled: () => {
402
+ copyHandled = true;
403
+ },
404
+ onCopied: copiedFromCache
405
+ });
406
+ function copiedFromCache() {
407
+ if (!errored || !noticeOnly || last === void 0)
408
+ return;
409
+ errored = false;
410
+ noticeOnly = false;
411
+ paint(last);
412
+ }
413
+ function cellAt(offsetX, offsetY, list) {
414
+ if (atlas === void 0)
415
+ return null;
416
+ const g = atlas.geometry;
417
+ const within = { width: window.innerWidth, height: window.innerHeight };
418
+ const scale = bestScale(list, within);
419
+ const at = center(list, within, scale);
420
+ const x = (offsetX - at.x) / scale;
421
+ const y = (offsetY - at.y) / scale;
422
+ if (x < 0 || y < 0)
423
+ return null;
424
+ const screenBottom = list.oia?.y ?? list.height;
425
+ const cols = Math.floor(list.width / g.cellWidth);
426
+ const rows = Math.floor(screenBottom / g.cellHeight);
427
+ const col = Math.floor(x / g.cellWidth);
428
+ const row = Math.floor(y / g.cellHeight);
429
+ if (col >= cols || row >= rows)
430
+ return null;
431
+ return { row, col };
432
+ }
433
+ canvas.addEventListener("mousedown", (e) => {
434
+ if (e.button !== 0)
435
+ return;
436
+ const list = last;
437
+ if (errored && !noticeOnly || list === void 0)
438
+ return;
439
+ const cell = cellAt(e.offsetX, e.offsetY, list);
440
+ if (cell === null)
441
+ return;
442
+ copyCache.invalidate();
443
+ anchor = cell;
444
+ focus = cell;
445
+ dragging = true;
446
+ pressAt = { x: e.clientX, y: e.clientY };
447
+ moved = false;
448
+ errored = false;
449
+ noticeOnly = false;
450
+ paint(list);
451
+ });
452
+ function release() {
453
+ dragging = false;
454
+ }
455
+ function prefetchSelection() {
456
+ const fetchSelection = window.tn3270.fetchSelection;
457
+ if (errored && !noticeOnly || fetchSelection === void 0)
458
+ return;
459
+ const rect = selectedRect();
460
+ if (rect === null)
461
+ return;
462
+ const gen = copyCache.begin(rect);
463
+ fetchSelection.call(window.tn3270, rect).then((text) => {
464
+ copyCache.accept(gen, text);
465
+ }).catch(() => {
466
+ });
467
+ }
468
+ window.addEventListener("mouseup", () => {
469
+ const ended = dragging;
470
+ release();
471
+ if (ended)
472
+ prefetchSelection();
473
+ });
474
+ window.addEventListener("blur", release);
475
+ window.addEventListener("mousemove", (e) => {
476
+ if (!dragging)
477
+ return;
478
+ const list = last;
479
+ if (list === void 0)
480
+ return;
481
+ let crossed = false;
482
+ if (!moved && pressAt !== null && movedPastThreshold(pressAt, { x: e.clientX, y: e.clientY })) {
483
+ moved = true;
484
+ crossed = true;
485
+ }
486
+ const cell = cellAt(e.offsetX, e.offsetY, list);
487
+ if (cell === null) {
488
+ if (crossed)
489
+ paint(list);
490
+ return;
491
+ }
492
+ if (!crossed && focus !== null && focus.row === cell.row && focus.col === cell.col)
493
+ return;
494
+ focus = cell;
495
+ paint(list);
496
+ });
497
+ window.addEventListener("resize", () => {
498
+ if (last !== void 0)
499
+ paint(last);
500
+ });
501
+ function copySelection() {
502
+ const rect = selectedRect();
503
+ if (rect === null)
504
+ return false;
505
+ window.tn3270.sendAction({ kind: "copy", rect });
506
+ return true;
507
+ }
508
+ function cachedSelection() {
509
+ const rect = selectedRect();
510
+ return rect === null ? void 0 : copyCache.take(rect);
511
+ }
512
+ function copyByChord() {
513
+ if (cachedSelection() !== void 0) {
514
+ copyHandled = false;
515
+ try {
516
+ document.execCommand("copy");
517
+ } catch {
518
+ }
519
+ if (copyHandled)
520
+ return;
521
+ }
522
+ copySelection();
523
+ }
524
+ window.__tn3270Copy = () => copySelection();
525
+ window.__tn3270CellCenter = (row, col) => {
526
+ const list = last;
527
+ if (list === void 0 || atlas === void 0)
528
+ return null;
529
+ const g = atlas.geometry;
530
+ const within = { width: window.innerWidth, height: window.innerHeight };
531
+ const scale = bestScale(list, within);
532
+ const at = center(list, within, scale);
533
+ const screenBottom = list.oia?.y ?? list.height;
534
+ if (row < 0 || col < 0)
535
+ return null;
536
+ if (col >= Math.floor(list.width / g.cellWidth))
537
+ return null;
538
+ if (row >= Math.floor(screenBottom / g.cellHeight))
539
+ return null;
540
+ return {
541
+ x: at.x + (col + 0.5) * g.cellWidth * scale,
542
+ y: at.y + (row + 0.5) * g.cellHeight * scale
543
+ };
544
+ };
545
+ function wrap(text, cols) {
546
+ const out = [];
547
+ let line = "";
548
+ for (const word of text.split(/\s+/)) {
549
+ if (line !== "" && `${line} ${word}`.length > cols) {
550
+ out.push(line);
551
+ line = word;
552
+ } else
553
+ line = line === "" ? word : `${line} ${word}`;
554
+ }
555
+ if (line !== "")
556
+ out.push(line);
557
+ return out;
558
+ }
559
+ //# sourceMappingURL=renderer.js.map