()
| 504 | } |
| 505 | |
| 506 | onRender() { |
| 507 | if (this.isUnmounted || this.isPaused) { |
| 508 | return; |
| 509 | } |
| 510 | // Entering a render cancels any pending drain tick — this render will |
| 511 | // handle the drain (and re-schedule below if needed). Prevents a |
| 512 | // wheel-event-triggered render AND a drain-timer render both firing. |
| 513 | if (this.drainTimer !== null) { |
| 514 | clearTimeout(this.drainTimer); |
| 515 | this.drainTimer = null; |
| 516 | } |
| 517 | |
| 518 | // Flush deferred interaction-time update before rendering so we call |
| 519 | // Date.now() at most once per frame instead of once per keypress. |
| 520 | // Done before the render to avoid dirtying state that would trigger |
| 521 | // an extra React re-render cycle. |
| 522 | this.options.onBeforeRender?.(); |
| 523 | |
| 524 | const renderStart = performance.now(); |
| 525 | const terminalWidth = this.options.stdout.columns || 80; |
| 526 | const terminalRows = this.options.stdout.rows || 24; |
| 527 | |
| 528 | const frame = this.renderer({ |
| 529 | frontFrame: this.frontFrame, |
| 530 | backFrame: this.backFrame, |
| 531 | isTTY: this.options.stdout.isTTY, |
| 532 | terminalWidth, |
| 533 | terminalRows, |
| 534 | altScreen: this.altScreenActive, |
| 535 | prevFrameContaminated: this.prevFrameContaminated, |
| 536 | }); |
| 537 | const rendererMs = performance.now() - renderStart; |
| 538 | |
| 539 | // Sticky/auto-follow scrolled the ScrollBox this frame. Translate the |
| 540 | // selection by the same delta so the highlight stays anchored to the |
| 541 | // TEXT (native terminal behavior — the selection walks up the screen |
| 542 | // as content scrolls, eventually clipping at the top). frontFrame |
| 543 | // still holds the PREVIOUS frame's screen (swap is at ~500 below), so |
| 544 | // captureScrolledRows reads the rows that are about to scroll out |
| 545 | // before they're overwritten — the text stays copyable until the |
| 546 | // selection scrolls entirely off. During drag, focus tracks the mouse |
| 547 | // (screen-local) so only anchor shifts — selection grows toward the |
| 548 | // mouse as the anchor walks up. After release, both ends are text- |
| 549 | // anchored and move as a block. |
| 550 | const follow = consumeFollowScroll(); |
| 551 | if ( |
| 552 | follow && |
| 553 | this.selection.anchor && |
| 554 | // Only translate if the selection is ON scrollbox content. Selections |
| 555 | // in the footer/prompt/StickyPromptHeader are on static text — the |
| 556 | // scroll doesn't move what's under them. Without this guard, a |
| 557 | // footer selection would be shifted by -delta then clamped to |
| 558 | // viewportBottom, teleporting it into the scrollbox. Mirror the |
| 559 | // bounds check the deleted check() in ScrollKeybindingHandler had. |
| 560 | this.selection.anchor.row >= follow.viewportTop && |
| 561 | this.selection.anchor.row <= follow.viewportBottom |
| 562 | ) { |
| 563 | const { delta, viewportTop, viewportBottom } = follow; |
no test coverage detected