| ... | @@ -631,6 +631,14 @@ export class GhosttyTerminalSurface { | ... | @@ -631,6 +631,14 @@ export class GhosttyTerminalSurface { |
| 631 | private clearSelectionAfterCopy = false; | 631 | private clearSelectionAfterCopy = false; |
| 632 | private primedCopySelection = ""; | 632 | private primedCopySelection = ""; |
| 633 | private wheelRemainder = 0; | 633 | private wheelRemainder = 0; |
| | 634 | private touchScroll: { |
| | 635 | pointerId: number; |
| | 636 | startX: number; |
| | 637 | startY: number; |
| | 638 | lastY: number; |
| | 639 | remainder: number; |
| | 640 | moved: boolean; |
| | 641 | } | null = null; |
| 634 | private lastMouseMotionData = ""; | 642 | private lastMouseMotionData = ""; |
| 635 | private mouseAnyEventTracking = false; | 643 | private mouseAnyEventTracking = false; |
| 636 | private dprMedia: MediaQueryList | null = null; | 644 | private dprMedia: MediaQueryList | null = null; |
| ... | @@ -680,6 +688,7 @@ export class GhosttyTerminalSurface { | ... | @@ -680,6 +688,7 @@ export class GhosttyTerminalSurface { |
| 680 | ): Promise<GhosttyTerminalSurface> { | 688 | ): Promise<GhosttyTerminalSurface> { |
| 681 | const canvas = document.createElement("canvas"); | 689 | const canvas = document.createElement("canvas"); |
| 682 | canvas.className = "block size-full cursor-text"; | 690 | canvas.className = "block size-full cursor-text"; |
| | 691 | canvas.style.touchAction = "pan-x pinch-zoom"; |
| 683 | canvas.setAttribute("aria-hidden", "true"); | 692 | canvas.setAttribute("aria-hidden", "true"); |
| 684 | | 693 | |
| 685 | const input = document.createElement("textarea"); | 694 | const input = document.createElement("textarea"); |
| ... | @@ -1361,6 +1370,21 @@ export class GhosttyTerminalSurface { | ... | @@ -1361,6 +1370,21 @@ export class GhosttyTerminalSurface { |
| 1361 | } | 1370 | } |
| 1362 | | 1371 | |
| 1363 | private readonly onPointerDown = (event: PointerEvent) => { | 1372 | private readonly onPointerDown = (event: PointerEvent) => { |
| | 1373 | if (event.pointerType === "touch") { |
| | 1374 | event.preventDefault(); |
| | 1375 | if (!event.isPrimary || this.touchScroll !== null) return; |
| | 1376 | this.touchScroll = { |
| | 1377 | pointerId: event.pointerId, |
| | 1378 | startX: event.clientX, |
| | 1379 | startY: event.clientY, |
| | 1380 | lastY: event.clientY, |
| | 1381 | remainder: 0, |
| | 1382 | moved: false, |
| | 1383 | }; |
| | 1384 | this.clearHoveredLink(); |
| | 1385 | this.canvas.setPointerCapture(event.pointerId); |
| | 1386 | return; |
| | 1387 | } |
| 1364 | this.focus(); | 1388 | this.focus(); |
| 1365 | if (shouldReportTerminalMouse(this.core.isMouseTracking(), event)) { | 1389 | if (shouldReportTerminalMouse(this.core.isMouseTracking(), event)) { |
| 1366 | const button = ghosttyMouseButton(event.button); | 1390 | const button = ghosttyMouseButton(event.button); |
| ... | @@ -1445,6 +1469,25 @@ export class GhosttyTerminalSurface { | ... | @@ -1445,6 +1469,25 @@ export class GhosttyTerminalSurface { |
| 1445 | } | 1469 | } |
| 1446 | | 1470 | |
| 1447 | private readonly onPointerMove = (event: PointerEvent) => { | 1471 | private readonly onPointerMove = (event: PointerEvent) => { |
| | 1472 | if (event.pointerType === "touch") { |
| | 1473 | const touch = this.touchScroll; |
| | 1474 | if (touch?.pointerId !== event.pointerId) return; |
| | 1475 | event.preventDefault(); |
| | 1476 | touch.moved ||= |
| | 1477 | Math.hypot(event.clientX - touch.startX, event.clientY - touch.startY) > |
| | 1478 | TERMINAL_LINK_DRAG_THRESHOLD_PX; |
| | 1479 | if (!touch.moved) return; |
| | 1480 | const delta = terminalWheelDeltaRows( |
| | 1481 | { deltaY: touch.lastY - event.clientY, deltaMode: 0 }, |
| | 1482 | this.metrics.height, |
| | 1483 | this.rows, |
| | 1484 | touch.remainder, |
| | 1485 | ); |
| | 1486 | touch.lastY = event.clientY; |
| | 1487 | touch.remainder = delta.remainder; |
| | 1488 | this.scrollRows(delta.rows, event); |
| | 1489 | return; |
| | 1490 | } |
| 1448 | if (this.linkActivationPointerId === event.pointerId) { | 1491 | if (this.linkActivationPointerId === event.pointerId) { |
| 1449 | const origin = this.linkActivationOrigin; | 1492 | const origin = this.linkActivationOrigin; |
| 1450 | if ( | 1493 | if ( |
| ... | @@ -1585,6 +1628,26 @@ export class GhosttyTerminalSurface { | ... | @@ -1585,6 +1628,26 @@ export class GhosttyTerminalSurface { |
| 1585 | } | 1628 | } |
| 1586 | | 1629 | |
| 1587 | private readonly onPointerUp = (event: PointerEvent) => { | 1630 | private readonly onPointerUp = (event: PointerEvent) => { |
| | 1631 | if (event.pointerType === "touch") { |
| | 1632 | const touch = this.touchScroll; |
| | 1633 | if (touch?.pointerId !== event.pointerId) return; |
| | 1634 | this.touchScroll = null; |
| | 1635 | event.preventDefault(); |
| | 1636 | if (this.canvas.hasPointerCapture(event.pointerId)) { |
| | 1637 | this.canvas.releasePointerCapture(event.pointerId); |
| | 1638 | } |
| | 1639 | if (event.type === "pointercancel" || touch.moved) return; |
| | 1640 | this.focus(); |
| | 1641 | if (shouldReportTerminalMouse(this.core.isMouseTracking(), event)) { |
| | 1642 | this.sendMouse("press", ghosttyMouseButton(0), event); |
| | 1643 | this.sendMouse("release", ghosttyMouseButton(0), event); |
| | 1644 | } else { |
| | 1645 | const link = this.linkAt(event.clientX, event.clientY); |
| | 1646 | if (link) this.options.onLinkActivate(link.text, event); |
| | 1647 | else this.clearSelection(); |
| | 1648 | } |
| | 1649 | return; |
| | 1650 | } |
| 1588 | this.setSelectionAutoscroll(0); | 1651 | this.setSelectionAutoscroll(0); |
| 1589 | if (this.linkActivationPointerId === event.pointerId) { | 1652 | if (this.linkActivationPointerId === event.pointerId) { |
| 1590 | event.preventDefault(); | 1653 | event.preventDefault(); |
| ... | @@ -1644,10 +1707,14 @@ export class GhosttyTerminalSurface { | ... | @@ -1644,10 +1707,14 @@ export class GhosttyTerminalSurface { |
| 1644 | this.wheelRemainder, | 1707 | this.wheelRemainder, |
| 1645 | ); | 1708 | ); |
| 1646 | this.wheelRemainder = delta.remainder; | 1709 | this.wheelRemainder = delta.remainder; |
| 1647 | if (delta.rows === 0) return; | 1710 | this.scrollRows(delta.rows, event); |
| 1648 | const magnitude = Math.abs(delta.rows); | 1711 | }; |
| | 1712 | |
| | 1713 | private scrollRows(rows: number, event: MouseEvent): void { |
| | 1714 | if (rows === 0) return; |
| | 1715 | const magnitude = Math.abs(rows); |
| 1649 | if (shouldReportTerminalMouse(this.core.isMouseTracking(), event)) { | 1716 | if (shouldReportTerminalMouse(this.core.isMouseTracking(), event)) { |
| 1650 | const button = delta.rows < 0 ? 4 : 5; | 1717 | const button = rows < 0 ? 4 : 5; |
| 1651 | for (let index = 0; index < magnitude; index += 1) { | 1718 | for (let index = 0; index < magnitude; index += 1) { |
| 1652 | this.sendMouse("press", button, event); | 1719 | this.sendMouse("press", button, event); |
| 1653 | } | 1720 | } |
| ... | @@ -1656,11 +1723,11 @@ export class GhosttyTerminalSurface { | ... | @@ -1656,11 +1723,11 @@ export class GhosttyTerminalSurface { |
| 1656 | if (this.core.isAlternateScreen()) { | 1723 | if (this.core.isAlternateScreen()) { |
| 1657 | // The alternate screen has no scrollback: translate wheel motion into | 1724 | // The alternate screen has no scrollback: translate wheel motion into |
| 1658 | // arrow keys so full-screen apps like vim and less scroll, matching xterm. | 1725 | // arrow keys so full-screen apps like vim and less scroll, matching xterm. |
| 1659 | this.options.onData(terminalWheelArrowData(delta.rows, this.core.isApplicationCursorKeys())); | 1726 | this.options.onData(terminalWheelArrowData(rows, this.core.isApplicationCursorKeys())); |
| 1660 | return; | 1727 | return; |
| 1661 | } | 1728 | } |
| 1662 | this.scrollViewport(delta.rows); | 1729 | this.scrollViewport(rows); |
| 1663 | }; | 1730 | } |
| 1664 | | 1731 | |
| 1665 | private readonly onMouseDown = (event: MouseEvent) => { | 1732 | private readonly onMouseDown = (event: MouseEvent) => { |
| 1666 | // Cancelling the middle button here stops autoscroll while still letting | 1733 | // Cancelling the middle button here stops autoscroll while still letting |