* Process mouse move for link detection (internal, called by throttled handler)
(e: MouseEvent)
| 1264 | * Process mouse move for link detection (internal, called by throttled handler) |
| 1265 | */ |
| 1266 | private processMouseMove(e: MouseEvent): void { |
| 1267 | if (!this.canvas || !this.renderer || !this.linkDetector || !this.wasmTerm) return; |
| 1268 | |
| 1269 | // Convert mouse coordinates to terminal cell position |
| 1270 | const rect = this.canvas.getBoundingClientRect(); |
| 1271 | const x = Math.floor((e.clientX - rect.left) / this.renderer.charWidth); |
| 1272 | const y = Math.floor((e.clientY - rect.top) / this.renderer.charHeight); |
| 1273 | |
| 1274 | // Get hyperlink_id directly from the cell at this position |
| 1275 | // Must account for viewportY (scrollback position) |
| 1276 | const viewportRow = y; // Row in the viewport (0 to rows-1) |
| 1277 | let hyperlinkId = 0; |
| 1278 | |
| 1279 | // When scrolled, fetch from scrollback or screen based on position |
| 1280 | // NOTE: viewportY may be fractional during smooth scrolling. The renderer |
| 1281 | // uses Math.floor(viewportY) when mapping viewport rows to scrollback vs |
| 1282 | // screen; we mirror that logic here so link hit-testing matches what the |
| 1283 | // user sees on screen. |
| 1284 | let line: GhosttyCell[] | null = null; |
| 1285 | const rawViewportY = this.getViewportY(); |
| 1286 | const viewportY = Math.max(0, Math.floor(rawViewportY)); |
| 1287 | if (viewportY > 0) { |
| 1288 | const scrollbackLength = this.wasmTerm.getScrollbackLength(); |
| 1289 | if (viewportRow < viewportY) { |
| 1290 | // Mouse is over scrollback content |
| 1291 | const scrollbackOffset = scrollbackLength - viewportY + viewportRow; |
| 1292 | line = this.wasmTerm.getScrollbackLine(scrollbackOffset); |
| 1293 | } else { |
| 1294 | // Mouse is over screen content (bottom part of viewport) |
| 1295 | const screenRow = viewportRow - viewportY; |
| 1296 | line = this.wasmTerm.getLine(screenRow); |
| 1297 | } |
| 1298 | } else { |
| 1299 | // At bottom - just use screen buffer |
| 1300 | line = this.wasmTerm.getLine(viewportRow); |
| 1301 | } |
| 1302 | |
| 1303 | if (line && x >= 0 && x < line.length) { |
| 1304 | hyperlinkId = line[x].hyperlink_id; |
| 1305 | } |
| 1306 | |
| 1307 | // Update renderer for underline rendering |
| 1308 | const previousHyperlinkId = (this.renderer as any).hoveredHyperlinkId || 0; |
| 1309 | if (hyperlinkId !== previousHyperlinkId) { |
| 1310 | this.renderer.setHoveredHyperlinkId(hyperlinkId); |
| 1311 | |
| 1312 | // The 60fps render loop will pick up the change automatically |
| 1313 | // No need to force a render - this keeps performance smooth |
| 1314 | } |
| 1315 | |
| 1316 | // Check if there's a link at this position (for click handling and cursor) |
| 1317 | // Buffer API expects absolute buffer coordinates (including scrollback) |
| 1318 | // When scrolled, we need to adjust the buffer row based on viewportY |
| 1319 | const scrollbackLength = this.wasmTerm.getScrollbackLength(); |
| 1320 | let bufferRow: number; |
| 1321 | |
| 1322 | // Use floored viewportY for buffer mapping (must match renderer & selection) |
| 1323 | const rawViewportYForBuffer = this.getViewportY(); |
no test coverage detected