restty 0.1.14 → 0.1.16

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/app/index.js CHANGED
@@ -50323,6 +50323,17 @@ function createRestty(options) {
50323
50323
  }
50324
50324
 
50325
50325
  // src/app/index.ts
50326
+ function normalizeTouchSelectionMode(value) {
50327
+ if (value === "drag" || value === "long-press" || value === "off")
50328
+ return value;
50329
+ return "long-press";
50330
+ }
50331
+ function clampFiniteNumber(value, fallback, min, max, round = false) {
50332
+ if (!Number.isFinite(value))
50333
+ return fallback;
50334
+ const numeric = round ? Math.round(value) : Number(value);
50335
+ return Math.min(max, Math.max(min, numeric));
50336
+ }
50326
50337
  function createResttyApp(options) {
50327
50338
  const { canvas: canvasInput, imeInput: imeInputInput, elements, callbacks } = options;
50328
50339
  const session = options.session ?? getDefaultResttyAppSession();
@@ -50345,6 +50356,12 @@ function createResttyApp(options) {
50345
50356
  const attachCanvasEvents = options.attachCanvasEvents ?? true;
50346
50357
  const autoResize = options.autoResize ?? true;
50347
50358
  const debugExpose = options.debugExpose ?? false;
50359
+ const touchSelectionMode = normalizeTouchSelectionMode(options.touchSelectionMode);
50360
+ const touchSelectionLongPressMs = clampFiniteNumber(options.touchSelectionLongPressMs, 450, 120, 2000, true);
50361
+ const touchSelectionMoveThresholdPx = clampFiniteNumber(options.touchSelectionMoveThresholdPx, 10, 1, 64);
50362
+ const hasCoarsePointer = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(any-pointer: coarse)").matches;
50363
+ const hasTouchPoints = typeof navigator !== "undefined" && navigator.maxTouchPoints > 0;
50364
+ const showOverlayScrollbar = !(hasCoarsePointer || hasTouchPoints);
50348
50365
  const nerdIconScale = Number.isFinite(options.nerdIconScale) ? Number(options.nerdIconScale) : 1;
50349
50366
  const alphaBlending = options.alphaBlending ?? "linear-corrected";
50350
50367
  const srgbChannelToLinear = (c3) => c3 <= 0.04045 ? c3 / 12.92 : Math.pow((c3 + 0.055) / 1.055, 2.4);
@@ -50546,6 +50563,17 @@ function createResttyApp(options) {
50546
50563
  anchor: null,
50547
50564
  focus: null
50548
50565
  };
50566
+ const touchSelectionState = {
50567
+ pendingPointerId: null,
50568
+ activePointerId: null,
50569
+ panPointerId: null,
50570
+ pendingCell: null,
50571
+ pendingStartedAt: 0,
50572
+ pendingStartX: 0,
50573
+ pendingStartY: 0,
50574
+ panLastY: 0,
50575
+ pendingTimer: 0
50576
+ };
50549
50577
  const linkState = {
50550
50578
  hoverId: 0,
50551
50579
  hoverUri: ""
@@ -50557,6 +50585,10 @@ function createResttyApp(options) {
50557
50585
  lastOffset: 0,
50558
50586
  lastLen: 0
50559
50587
  };
50588
+ const scrollbarDragState = {
50589
+ pointerId: null,
50590
+ thumbGrabRatio: 0.5
50591
+ };
50560
50592
  const KITTY_FLAG_REPORT_EVENTS2 = 1 << 1;
50561
50593
  function updateCanvasCursor() {
50562
50594
  if (!canvas)
@@ -50567,9 +50599,200 @@ function createResttyApp(options) {
50567
50599
  }
50568
50600
  canvas.style.cursor = linkState.hoverId ? "pointer" : "default";
50569
50601
  }
50602
+ function isTouchPointer(event) {
50603
+ return event.pointerType === "touch";
50604
+ }
50605
+ function clearPendingTouchSelection() {
50606
+ if (touchSelectionState.pendingTimer) {
50607
+ clearTimeout(touchSelectionState.pendingTimer);
50608
+ touchSelectionState.pendingTimer = 0;
50609
+ }
50610
+ touchSelectionState.pendingPointerId = null;
50611
+ touchSelectionState.pendingCell = null;
50612
+ touchSelectionState.pendingStartedAt = 0;
50613
+ }
50614
+ function tryActivatePendingTouchSelection(pointerId) {
50615
+ if (touchSelectionMode !== "long-press")
50616
+ return false;
50617
+ if (touchSelectionState.pendingPointerId !== pointerId || !touchSelectionState.pendingCell) {
50618
+ return false;
50619
+ }
50620
+ if (performance.now() - touchSelectionState.pendingStartedAt < touchSelectionLongPressMs) {
50621
+ return false;
50622
+ }
50623
+ const pendingCell = touchSelectionState.pendingCell;
50624
+ clearPendingTouchSelection();
50625
+ beginSelectionDrag(pendingCell, pointerId);
50626
+ return true;
50627
+ }
50628
+ function beginSelectionDrag(cell, pointerId) {
50629
+ selectionState.active = true;
50630
+ selectionState.dragging = true;
50631
+ selectionState.anchor = cell;
50632
+ selectionState.focus = cell;
50633
+ touchSelectionState.activePointerId = pointerId;
50634
+ touchSelectionState.panPointerId = null;
50635
+ canvas.setPointerCapture?.(pointerId);
50636
+ updateCanvasCursor();
50637
+ needsRender = true;
50638
+ }
50570
50639
  function noteScrollActivity() {
50571
50640
  scrollbarState.lastInputAt = performance.now();
50572
50641
  }
50642
+ function getViewportScrollOffset() {
50643
+ if (!wasmHandle || !wasmExports?.restty_scrollbar_offset)
50644
+ return 0;
50645
+ return wasmExports.restty_scrollbar_offset(wasmHandle) || 0;
50646
+ }
50647
+ function shiftSelectionByRows(deltaRows) {
50648
+ if (!deltaRows)
50649
+ return;
50650
+ if (!selectionState.active && !selectionState.dragging)
50651
+ return;
50652
+ if (!selectionState.anchor || !selectionState.focus)
50653
+ return;
50654
+ const maxAbs = Math.max(1024, (gridState.rows || 24) * 128);
50655
+ selectionState.anchor = {
50656
+ row: clamp(selectionState.anchor.row + deltaRows, -maxAbs, maxAbs),
50657
+ col: selectionState.anchor.col
50658
+ };
50659
+ selectionState.focus = {
50660
+ row: clamp(selectionState.focus.row + deltaRows, -maxAbs, maxAbs),
50661
+ col: selectionState.focus.col
50662
+ };
50663
+ needsRender = true;
50664
+ }
50665
+ function scrollViewportByLines(lines) {
50666
+ if (!wasmReady || !wasmHandle || !gridState.cellH)
50667
+ return;
50668
+ scrollRemainder += lines;
50669
+ const delta = Math.trunc(scrollRemainder);
50670
+ scrollRemainder -= delta;
50671
+ if (!delta)
50672
+ return;
50673
+ const beforeOffset = getViewportScrollOffset();
50674
+ wasm.scrollViewport(wasmHandle, delta);
50675
+ const afterOffset = getViewportScrollOffset();
50676
+ shiftSelectionByRows(beforeOffset - afterOffset);
50677
+ if (linkState.hoverId)
50678
+ updateLinkHover(null);
50679
+ wasm.renderUpdate(wasmHandle);
50680
+ needsRender = true;
50681
+ noteScrollActivity();
50682
+ }
50683
+ function setViewportScrollOffset(nextOffset) {
50684
+ if (!wasmReady || !wasmHandle || !wasmExports?.restty_scrollbar_total)
50685
+ return;
50686
+ const total = wasmExports.restty_scrollbar_total(wasmHandle) || 0;
50687
+ const len = wasmExports.restty_scrollbar_len ? wasmExports.restty_scrollbar_len(wasmHandle) : 0;
50688
+ const current = wasmExports.restty_scrollbar_offset ? wasmExports.restty_scrollbar_offset(wasmHandle) : 0;
50689
+ const maxOffset = Math.max(0, total - len);
50690
+ const clamped = clamp(Math.round(nextOffset), 0, maxOffset);
50691
+ const delta = clamped - current;
50692
+ if (!delta)
50693
+ return;
50694
+ const beforeOffset = getViewportScrollOffset();
50695
+ wasm.scrollViewport(wasmHandle, delta);
50696
+ const afterOffset = getViewportScrollOffset();
50697
+ shiftSelectionByRows(beforeOffset - afterOffset);
50698
+ if (linkState.hoverId)
50699
+ updateLinkHover(null);
50700
+ wasm.renderUpdate(wasmHandle);
50701
+ needsRender = true;
50702
+ noteScrollActivity();
50703
+ }
50704
+ function pointerToCanvasPx(event) {
50705
+ const rect = canvas.getBoundingClientRect();
50706
+ const scaleX = rect.width > 0 ? canvas.width / rect.width : 1;
50707
+ const scaleY = rect.height > 0 ? canvas.height / rect.height : 1;
50708
+ return {
50709
+ x: (event.clientX - rect.left) * scaleX,
50710
+ y: (event.clientY - rect.top) * scaleY
50711
+ };
50712
+ }
50713
+ function computeOverlayScrollbarLayout(rows, cellW, cellH, total, offset, len) {
50714
+ if (!(total > len && len > 0))
50715
+ return null;
50716
+ const trackHeight = rows * cellH;
50717
+ const width = Math.max(10, Math.round(cellW * 0.9));
50718
+ const margin = Math.max(6, Math.round(width * 0.5));
50719
+ const insetY = Math.max(2, Math.round(cellH * 0.2));
50720
+ const trackX = canvas.width - margin - width;
50721
+ const trackY = insetY;
50722
+ const trackH = Math.max(width, trackHeight - insetY * 2);
50723
+ const denom = Math.max(1, total - len);
50724
+ const thumbH = Math.max(width, Math.round(trackH * (len / total)));
50725
+ const thumbY = trackY + Math.round(offset / denom * (trackH - thumbH));
50726
+ return { total, offset, len, denom, width, trackX, trackY, trackH, thumbY, thumbH };
50727
+ }
50728
+ function getOverlayScrollbarLayout() {
50729
+ if (!showOverlayScrollbar || !wasmExports?.restty_scrollbar_total || !wasmHandle)
50730
+ return null;
50731
+ if (!gridState.rows || !gridState.cellW || !gridState.cellH)
50732
+ return null;
50733
+ const total = wasmExports.restty_scrollbar_total(wasmHandle) || 0;
50734
+ const offset = wasmExports.restty_scrollbar_offset ? wasmExports.restty_scrollbar_offset(wasmHandle) : 0;
50735
+ const len = wasmExports.restty_scrollbar_len ? wasmExports.restty_scrollbar_len(wasmHandle) : gridState.rows;
50736
+ return computeOverlayScrollbarLayout(gridState.rows, gridState.cellW, gridState.cellH, total, offset, len);
50737
+ }
50738
+ function isPointInScrollbarHitArea(layout, x3, y) {
50739
+ const hitPadX = Math.max(3, Math.round(layout.width * 0.35));
50740
+ return x3 >= layout.trackX - hitPadX && x3 <= layout.trackX + layout.width + hitPadX && y >= layout.trackY && y <= layout.trackY + layout.trackH;
50741
+ }
50742
+ function isPointInScrollbarThumb(layout, x3, y) {
50743
+ return x3 >= layout.trackX && x3 <= layout.trackX + layout.width && y >= layout.thumbY && y <= layout.thumbY + layout.thumbH;
50744
+ }
50745
+ function scrollbarOffsetForPointerY(layout, pointerY, thumbGrabRatio) {
50746
+ const thumbTop = pointerY - layout.thumbH * thumbGrabRatio;
50747
+ const trackSpan = Math.max(1, layout.trackH - layout.thumbH);
50748
+ const ratio = clamp((thumbTop - layout.trackY) / trackSpan, 0, 1);
50749
+ return Math.round(ratio * layout.denom);
50750
+ }
50751
+ function pushRoundedVerticalBar(out, x3, y, w, h, color) {
50752
+ const x02 = Math.round(x3);
50753
+ const y02 = Math.round(y);
50754
+ const width = Math.max(1, Math.round(w));
50755
+ const height = Math.max(1, Math.round(h));
50756
+ const radius = Math.min(Math.floor(width * 0.5), Math.floor(height * 0.5));
50757
+ if (radius <= 0) {
50758
+ pushRectBox(out, x02, y02, width, height, color);
50759
+ return;
50760
+ }
50761
+ const middleH = height - radius * 2;
50762
+ if (middleH > 0) {
50763
+ pushRectBox(out, x02, y02 + radius, width, middleH, color);
50764
+ }
50765
+ const radiusSq = radius * radius;
50766
+ for (let row = 0;row < radius; row += 1) {
50767
+ const dy = radius - row - 0.5;
50768
+ const inset = Math.max(0, Math.floor(radius - Math.sqrt(Math.max(0, radiusSq - dy * dy))));
50769
+ const rowW = width - inset * 2;
50770
+ if (rowW <= 0)
50771
+ continue;
50772
+ pushRectBox(out, x02 + inset, y02 + row, rowW, 1, color);
50773
+ pushRectBox(out, x02 + inset, y02 + height - 1 - row, rowW, 1, color);
50774
+ }
50775
+ }
50776
+ function appendOverlayScrollbar(overlayData, rows, cellW, cellH, total, offset, len) {
50777
+ if (!showOverlayScrollbar)
50778
+ return;
50779
+ const layout = computeOverlayScrollbarLayout(rows, cellW, cellH, total, offset, len);
50780
+ if (!layout)
50781
+ return;
50782
+ const since = performance.now() - scrollbarState.lastInputAt;
50783
+ const fadeDelay = 160;
50784
+ const fadeDuration = 520;
50785
+ let alpha = 0;
50786
+ if (since < fadeDelay) {
50787
+ alpha = 0.68;
50788
+ } else if (since < fadeDelay + fadeDuration) {
50789
+ alpha = 0.68 * (1 - (since - fadeDelay) / fadeDuration);
50790
+ }
50791
+ if (alpha <= 0.01)
50792
+ return;
50793
+ const thumbColor = [0.96, 0.96, 0.96, alpha * 0.75];
50794
+ pushRoundedVerticalBar(overlayData, layout.trackX, layout.thumbY, layout.width, layout.thumbH, thumbColor);
50795
+ }
50573
50796
  function releaseKittyImage(entry) {
50574
50797
  const source = entry?.source;
50575
50798
  if (source && typeof source.close === "function") {
@@ -51167,6 +51390,7 @@ function createResttyApp(options) {
51167
51390
  selectionState.dragging = false;
51168
51391
  selectionState.anchor = null;
51169
51392
  selectionState.focus = null;
51393
+ touchSelectionState.activePointerId = null;
51170
51394
  updateCanvasCursor();
51171
51395
  needsRender = true;
51172
51396
  }
@@ -51380,30 +51604,116 @@ function createResttyApp(options) {
51380
51604
  function bindCanvasEvents() {
51381
51605
  if (!attachCanvasEvents)
51382
51606
  return;
51607
+ canvas.style.touchAction = touchSelectionMode === "long-press" || touchSelectionMode === "drag" ? "none" : "pan-y pinch-zoom";
51383
51608
  const onPointerDown = (event) => {
51609
+ if (!isTouchPointer(event) && event.button === 0) {
51610
+ const layout = getOverlayScrollbarLayout();
51611
+ if (layout) {
51612
+ const point = pointerToCanvasPx(event);
51613
+ if (isPointInScrollbarHitArea(layout, point.x, point.y)) {
51614
+ event.preventDefault();
51615
+ noteScrollActivity();
51616
+ const hitThumb = isPointInScrollbarThumb(layout, point.x, point.y);
51617
+ scrollbarDragState.pointerId = event.pointerId;
51618
+ scrollbarDragState.thumbGrabRatio = hitThumb ? clamp((point.y - layout.thumbY) / Math.max(1, layout.thumbH), 0, 1) : 0.5;
51619
+ const targetOffset = scrollbarOffsetForPointerY(layout, point.y, scrollbarDragState.thumbGrabRatio);
51620
+ setViewportScrollOffset(targetOffset);
51621
+ canvas.setPointerCapture?.(event.pointerId);
51622
+ return;
51623
+ }
51624
+ }
51625
+ }
51384
51626
  if (inputHandler.sendMouseEvent("down", event)) {
51385
51627
  event.preventDefault();
51386
51628
  canvas.setPointerCapture?.(event.pointerId);
51387
51629
  return;
51388
51630
  }
51631
+ if (isTouchPointer(event)) {
51632
+ if (event.button !== 0)
51633
+ return;
51634
+ const cell2 = normalizeSelectionCell(positionToCell(event));
51635
+ touchSelectionState.activePointerId = null;
51636
+ touchSelectionState.panPointerId = null;
51637
+ if (touchSelectionMode === "off")
51638
+ return;
51639
+ if (touchSelectionMode === "drag") {
51640
+ event.preventDefault();
51641
+ beginSelectionDrag(cell2, event.pointerId);
51642
+ return;
51643
+ }
51644
+ clearPendingTouchSelection();
51645
+ touchSelectionState.pendingPointerId = event.pointerId;
51646
+ touchSelectionState.pendingCell = cell2;
51647
+ touchSelectionState.pendingStartedAt = performance.now();
51648
+ touchSelectionState.pendingStartX = event.clientX;
51649
+ touchSelectionState.pendingStartY = event.clientY;
51650
+ touchSelectionState.panPointerId = event.pointerId;
51651
+ touchSelectionState.panLastY = event.clientY;
51652
+ touchSelectionState.pendingTimer = setTimeout(() => {
51653
+ tryActivatePendingTouchSelection(event.pointerId);
51654
+ }, touchSelectionLongPressMs);
51655
+ return;
51656
+ }
51389
51657
  if (event.button !== 0)
51390
51658
  return;
51391
51659
  event.preventDefault();
51392
51660
  const cell = normalizeSelectionCell(positionToCell(event));
51393
51661
  updateLinkHover(cell);
51394
- selectionState.active = true;
51395
- selectionState.dragging = true;
51396
- selectionState.anchor = cell;
51397
- selectionState.focus = cell;
51398
- canvas.setPointerCapture?.(event.pointerId);
51399
- updateCanvasCursor();
51400
- needsRender = true;
51662
+ beginSelectionDrag(cell, event.pointerId);
51401
51663
  };
51402
51664
  const onPointerMove = (event) => {
51665
+ if (scrollbarDragState.pointerId === event.pointerId) {
51666
+ const layout = getOverlayScrollbarLayout();
51667
+ if (!layout) {
51668
+ scrollbarDragState.pointerId = null;
51669
+ return;
51670
+ }
51671
+ const point = pointerToCanvasPx(event);
51672
+ const targetOffset = scrollbarOffsetForPointerY(layout, point.y, scrollbarDragState.thumbGrabRatio);
51673
+ setViewportScrollOffset(targetOffset);
51674
+ event.preventDefault();
51675
+ return;
51676
+ }
51403
51677
  if (inputHandler.sendMouseEvent("move", event)) {
51404
51678
  event.preventDefault();
51405
51679
  return;
51406
51680
  }
51681
+ if (isTouchPointer(event)) {
51682
+ if (touchSelectionState.pendingPointerId === event.pointerId) {
51683
+ const dx = event.clientX - touchSelectionState.pendingStartX;
51684
+ const dy = event.clientY - touchSelectionState.pendingStartY;
51685
+ if (dx * dx + dy * dy >= touchSelectionMoveThresholdPx * touchSelectionMoveThresholdPx) {
51686
+ clearPendingTouchSelection();
51687
+ } else {
51688
+ tryActivatePendingTouchSelection(event.pointerId);
51689
+ }
51690
+ if (touchSelectionState.pendingPointerId === event.pointerId) {
51691
+ if (touchSelectionMode === "long-press" && touchSelectionState.panPointerId === event.pointerId) {
51692
+ const deltaPx = touchSelectionState.panLastY - event.clientY;
51693
+ touchSelectionState.panLastY = event.clientY;
51694
+ scrollViewportByLines(deltaPx / Math.max(1, gridState.cellH) * 1.5);
51695
+ event.preventDefault();
51696
+ }
51697
+ return;
51698
+ }
51699
+ }
51700
+ if (selectionState.dragging && touchSelectionState.activePointerId === event.pointerId) {
51701
+ const cell2 = normalizeSelectionCell(positionToCell(event));
51702
+ event.preventDefault();
51703
+ selectionState.focus = cell2;
51704
+ updateLinkHover(null);
51705
+ updateCanvasCursor();
51706
+ needsRender = true;
51707
+ return;
51708
+ }
51709
+ if (touchSelectionMode === "long-press" && touchSelectionState.panPointerId === event.pointerId) {
51710
+ const deltaPx = touchSelectionState.panLastY - event.clientY;
51711
+ touchSelectionState.panLastY = event.clientY;
51712
+ scrollViewportByLines(deltaPx / Math.max(1, gridState.cellH) * 1.5);
51713
+ event.preventDefault();
51714
+ }
51715
+ return;
51716
+ }
51407
51717
  const cell = normalizeSelectionCell(positionToCell(event));
51408
51718
  if (!selectionState.dragging) {
51409
51719
  updateLinkHover(cell);
@@ -51416,10 +51726,41 @@ function createResttyApp(options) {
51416
51726
  needsRender = true;
51417
51727
  };
51418
51728
  const onPointerUp = (event) => {
51729
+ if (scrollbarDragState.pointerId === event.pointerId) {
51730
+ scrollbarDragState.pointerId = null;
51731
+ event.preventDefault();
51732
+ return;
51733
+ }
51419
51734
  if (inputHandler.sendMouseEvent("up", event)) {
51420
51735
  event.preventDefault();
51421
51736
  return;
51422
51737
  }
51738
+ if (isTouchPointer(event)) {
51739
+ if (touchSelectionState.pendingPointerId === event.pointerId) {
51740
+ clearPendingTouchSelection();
51741
+ touchSelectionState.activePointerId = null;
51742
+ touchSelectionState.panPointerId = null;
51743
+ return;
51744
+ }
51745
+ if (selectionState.dragging && touchSelectionState.activePointerId === event.pointerId) {
51746
+ const cell2 = normalizeSelectionCell(positionToCell(event));
51747
+ event.preventDefault();
51748
+ selectionState.dragging = false;
51749
+ selectionState.focus = cell2;
51750
+ touchSelectionState.activePointerId = null;
51751
+ if (selectionState.anchor && selectionState.focus && selectionState.anchor.row === selectionState.focus.row && selectionState.anchor.col === selectionState.focus.col) {
51752
+ clearSelection();
51753
+ } else {
51754
+ updateCanvasCursor();
51755
+ needsRender = true;
51756
+ }
51757
+ return;
51758
+ }
51759
+ if (touchSelectionState.panPointerId === event.pointerId) {
51760
+ touchSelectionState.panPointerId = null;
51761
+ }
51762
+ return;
51763
+ }
51423
51764
  const cell = normalizeSelectionCell(positionToCell(event));
51424
51765
  if (selectionState.dragging) {
51425
51766
  event.preventDefault();
@@ -51434,10 +51775,31 @@ function createResttyApp(options) {
51434
51775
  } else {
51435
51776
  updateLinkHover(cell);
51436
51777
  }
51437
- if (!selectionState.active && event.button === 0 && (event.metaKey || event.ctrlKey) && linkState.hoverUri) {
51778
+ if (!selectionState.active && event.button === 0 && linkState.hoverUri) {
51438
51779
  openLink(linkState.hoverUri);
51439
51780
  }
51440
51781
  };
51782
+ const onPointerCancel = (event) => {
51783
+ if (scrollbarDragState.pointerId === event.pointerId) {
51784
+ scrollbarDragState.pointerId = null;
51785
+ }
51786
+ if (isTouchPointer(event)) {
51787
+ if (touchSelectionState.pendingPointerId === event.pointerId) {
51788
+ clearPendingTouchSelection();
51789
+ }
51790
+ if (touchSelectionState.panPointerId === event.pointerId) {
51791
+ touchSelectionState.panPointerId = null;
51792
+ }
51793
+ if (touchSelectionState.activePointerId === event.pointerId) {
51794
+ touchSelectionState.activePointerId = null;
51795
+ if (selectionState.dragging) {
51796
+ selectionState.dragging = false;
51797
+ updateCanvasCursor();
51798
+ needsRender = true;
51799
+ }
51800
+ }
51801
+ }
51802
+ };
51441
51803
  const onWheel = (event) => {
51442
51804
  const mouseActive = inputHandler.isMouseActive();
51443
51805
  const altScreen = inputHandler.isAltScreen ? inputHandler.isAltScreen() : false;
@@ -51458,16 +51820,7 @@ function createResttyApp(options) {
51458
51820
  } else {
51459
51821
  lines = event.deltaY / gridState.cellH;
51460
51822
  }
51461
- lines *= speed;
51462
- scrollRemainder += lines;
51463
- const delta = Math.trunc(scrollRemainder);
51464
- scrollRemainder -= delta;
51465
- if (!delta)
51466
- return;
51467
- wasm.scrollViewport(wasmHandle, delta);
51468
- wasm.renderUpdate(wasmHandle);
51469
- needsRender = true;
51470
- noteScrollActivity();
51823
+ scrollViewportByLines(lines * speed);
51471
51824
  event.preventDefault();
51472
51825
  };
51473
51826
  const onContextMenu = (event) => {
@@ -51480,6 +51833,7 @@ function createResttyApp(options) {
51480
51833
  canvas.addEventListener("pointerdown", onPointerDown);
51481
51834
  canvas.addEventListener("pointermove", onPointerMove);
51482
51835
  canvas.addEventListener("pointerup", onPointerUp);
51836
+ canvas.addEventListener("pointercancel", onPointerCancel);
51483
51837
  canvas.addEventListener("pointerleave", onPointerLeave);
51484
51838
  canvas.addEventListener("wheel", onWheel, { passive: false });
51485
51839
  canvas.addEventListener("contextmenu", onContextMenu);
@@ -51487,9 +51841,12 @@ function createResttyApp(options) {
51487
51841
  canvas.removeEventListener("pointerdown", onPointerDown);
51488
51842
  canvas.removeEventListener("pointermove", onPointerMove);
51489
51843
  canvas.removeEventListener("pointerup", onPointerUp);
51844
+ canvas.removeEventListener("pointercancel", onPointerCancel);
51490
51845
  canvas.removeEventListener("pointerleave", onPointerLeave);
51491
51846
  canvas.removeEventListener("wheel", onWheel);
51492
51847
  canvas.removeEventListener("contextmenu", onContextMenu);
51848
+ clearPendingTouchSelection();
51849
+ scrollbarDragState.pointerId = null;
51493
51850
  });
51494
51851
  if (imeInput) {
51495
51852
  let suppressNextInput = false;
@@ -53632,7 +53989,9 @@ function createResttyApp(options) {
53632
53989
  const startRow = forward ? a3.row : f.row;
53633
53990
  const endRow = forward ? f.row : a3.row;
53634
53991
  const lines = [];
53635
- for (let row = startRow;row <= endRow; row += 1) {
53992
+ const clampedStartRow = clamp(startRow, 0, rows - 1);
53993
+ const clampedEndRow = clamp(endRow, 0, rows - 1);
53994
+ for (let row = clampedStartRow;row <= clampedEndRow; row += 1) {
53636
53995
  const range = selectionForRow(row, cols);
53637
53996
  if (!range)
53638
53997
  continue;
@@ -53759,8 +54118,6 @@ function createResttyApp(options) {
53759
54118
  graphemeOffset,
53760
54119
  graphemeLen,
53761
54120
  graphemeBuffer,
53762
- selectionStart,
53763
- selectionEnd,
53764
54121
  cursor
53765
54122
  } = render;
53766
54123
  if (!codepoints || !fgBytes)
@@ -53951,13 +54308,9 @@ function createResttyApp(options) {
53951
54308
  for (let row = 0;row < rows; row += 1) {
53952
54309
  const rowY = row * cellH;
53953
54310
  const baseY = rowY + yPad + baselineOffset;
53954
- let selStart = selectionStart ? selectionStart[row] : -1;
53955
- let selEnd = selectionEnd ? selectionEnd[row] : -1;
53956
54311
  const localSel = selectionState.active ? selectionForRow(row, cols) : null;
53957
- if (localSel) {
53958
- selStart = localSel.start;
53959
- selEnd = localSel.end;
53960
- }
54312
+ const selStart = localSel?.start ?? -1;
54313
+ const selEnd = localSel?.end ?? -1;
53961
54314
  if (selStart >= 0 && selEnd > selStart) {
53962
54315
  const start = Math.max(0, selStart);
53963
54316
  const end = Math.min(cols, selEnd);
@@ -54465,36 +54818,8 @@ function createResttyApp(options) {
54465
54818
  scrollbarState.lastTotal = total;
54466
54819
  scrollbarState.lastOffset = offset;
54467
54820
  scrollbarState.lastLen = len;
54468
- noteScrollActivity();
54469
- }
54470
- if (total > len && len > 0) {
54471
- const now = performance.now();
54472
- const since = now - scrollbarState.lastInputAt;
54473
- const fadeDelay = 600;
54474
- const fadeDuration = 700;
54475
- let alpha = 0;
54476
- if (offset > 0) {
54477
- alpha = 0.65;
54478
- } else if (since < fadeDelay) {
54479
- alpha = 0.5;
54480
- } else if (since < fadeDelay + fadeDuration) {
54481
- alpha = 0.5 * (1 - (since - fadeDelay) / fadeDuration);
54482
- }
54483
- if (alpha > 0.01) {
54484
- const trackH = rows * cellH;
54485
- const scrollbarWidth = Math.max(2, Math.round(cellW * 0.12));
54486
- const margin = Math.max(2, Math.round(cellW * 0.2));
54487
- const trackX = canvas.width - margin - scrollbarWidth;
54488
- const trackY = 0;
54489
- const denom = Math.max(1, total - len);
54490
- const thumbH = Math.max(cellH * 1.5, Math.round(trackH * (len / total)));
54491
- const thumbY = Math.round(offset / denom * (trackH - thumbH));
54492
- const thumbColor = [1, 1, 1, alpha * 0.35];
54493
- const trackColor = [1, 1, 1, alpha * 0.08];
54494
- pushRectBox(overlayData, trackX, trackY, scrollbarWidth, trackH, trackColor);
54495
- pushRectBox(overlayData, trackX, thumbY, scrollbarWidth, thumbH, thumbColor);
54496
- }
54497
54821
  }
54822
+ appendOverlayScrollbar(overlayData, rows, cellW, cellH, total, offset, len);
54498
54823
  }
54499
54824
  webgpuUniforms[0] = canvas.width;
54500
54825
  webgpuUniforms[1] = canvas.height;
@@ -54670,8 +54995,6 @@ function createResttyApp(options) {
54670
54995
  graphemeOffset,
54671
54996
  graphemeLen,
54672
54997
  graphemeBuffer,
54673
- selectionStart,
54674
- selectionEnd,
54675
54998
  cursor
54676
54999
  } = render;
54677
55000
  if (!codepoints || !fgBytes) {
@@ -54849,13 +55172,9 @@ function createResttyApp(options) {
54849
55172
  for (let row = 0;row < rows; row += 1) {
54850
55173
  const rowY = row * cellH;
54851
55174
  const baseY = rowY + yPad + baselineOffset;
54852
- let selStart = selectionStart ? selectionStart[row] : -1;
54853
- let selEnd = selectionEnd ? selectionEnd[row] : -1;
54854
55175
  const localSel = selectionState.active ? selectionForRow(row, cols) : null;
54855
- if (localSel) {
54856
- selStart = localSel.start;
54857
- selEnd = localSel.end;
54858
- }
55176
+ const selStart = localSel?.start ?? -1;
55177
+ const selEnd = localSel?.end ?? -1;
54859
55178
  if (selStart >= 0 && selEnd > selStart) {
54860
55179
  const start = Math.max(0, selStart);
54861
55180
  const end = Math.min(cols, selEnd);
@@ -55189,36 +55508,8 @@ function createResttyApp(options) {
55189
55508
  scrollbarState.lastTotal = total;
55190
55509
  scrollbarState.lastOffset = offset;
55191
55510
  scrollbarState.lastLen = len;
55192
- noteScrollActivity();
55193
- }
55194
- if (total > len && len > 0) {
55195
- const now = performance.now();
55196
- const since = now - scrollbarState.lastInputAt;
55197
- const fadeDelay = 600;
55198
- const fadeDuration = 700;
55199
- let alpha = 0;
55200
- if (offset > 0) {
55201
- alpha = 0.65;
55202
- } else if (since < fadeDelay) {
55203
- alpha = 0.5;
55204
- } else if (since < fadeDelay + fadeDuration) {
55205
- alpha = 0.5 * (1 - (since - fadeDelay) / fadeDuration);
55206
- }
55207
- if (alpha > 0.01) {
55208
- const trackH = rows * cellH;
55209
- const scrollbarWidth = Math.max(2, Math.round(cellW * 0.12));
55210
- const margin = Math.max(2, Math.round(cellW * 0.2));
55211
- const trackX = canvas.width - margin - scrollbarWidth;
55212
- const trackY = 0;
55213
- const denom = Math.max(1, total - len);
55214
- const thumbH = Math.max(cellH * 1.5, Math.round(trackH * (len / total)));
55215
- const thumbY = Math.round(offset / denom * (trackH - thumbH));
55216
- const thumbColor = [1, 1, 1, alpha * 0.35];
55217
- const trackColor = [1, 1, 1, alpha * 0.08];
55218
- pushRectBox(overlayData, trackX, trackY, scrollbarWidth, trackH, trackColor);
55219
- pushRectBox(overlayData, trackX, thumbY, scrollbarWidth, thumbH, thumbColor);
55220
- }
55221
55511
  }
55512
+ appendOverlayScrollbar(overlayData, rows, cellW, cellH, total, offset, len);
55222
55513
  }
55223
55514
  for (const [fontIndex, neededIds] of neededGlyphIdsByFont.entries()) {
55224
55515
  const fontEntry = fontState.fonts[fontIndex];
@@ -55594,9 +55885,11 @@ function createResttyApp(options) {
55594
55885
  }
55595
55886
  appendLog(`[key] ${JSON.stringify(normalized)}${before}`);
55596
55887
  }
55597
- if (source === "key" && selectionState.active) {
55888
+ if (source !== "program" && (selectionState.active || selectionState.dragging)) {
55598
55889
  clearSelection();
55599
55890
  }
55891
+ if (source === "pty" && linkState.hoverId)
55892
+ updateLinkHover(null);
55600
55893
  writeToWasm(wasmHandle, normalized);
55601
55894
  flushWasmOutputToPty();
55602
55895
  if (source === "pty" && inputHandler?.isSynchronizedOutput?.()) {
@@ -63,6 +63,7 @@ export type ResttyLocalFontSource = {
63
63
  export type ResttyFontSource = ResttyUrlFontSource | ResttyBufferFontSource | ResttyLocalFontSource;
64
64
  export type FontSource = ResttyFontSource;
65
65
  export type ResttyFontPreset = "default-cdn" | "none";
66
+ export type ResttyTouchSelectionMode = "drag" | "long-press" | "off";
66
67
  export type ResttyAppOptions = {
67
68
  canvas: HTMLCanvasElement;
68
69
  session?: ResttyAppSession;
@@ -83,6 +84,23 @@ export type ResttyAppOptions = {
83
84
  autoResize?: boolean;
84
85
  attachWindowEvents?: boolean;
85
86
  attachCanvasEvents?: boolean;
87
+ /**
88
+ * Touch selection behavior on pointerType=touch:
89
+ * - drag: immediate drag-selection (legacy behavior)
90
+ * - long-press: selection starts after press timeout (default)
91
+ * - off: disable touch selection, keep touch scrolling
92
+ */
93
+ touchSelectionMode?: ResttyTouchSelectionMode;
94
+ /**
95
+ * Long-press timeout in ms for touch selection intent.
96
+ * Only used when touchSelectionMode is "long-press".
97
+ */
98
+ touchSelectionLongPressMs?: number;
99
+ /**
100
+ * Pointer move threshold in CSS pixels before long-press selection is
101
+ * canceled and touch pan-scroll takes priority.
102
+ */
103
+ touchSelectionMoveThresholdPx?: number;
86
104
  debugExpose?: boolean;
87
105
  ptyTransport?: PtyTransport;
88
106
  };