Skip to content

fix(scroll): correctly calculate absolute Y for nested regions#126

Merged
jacob314 merged 6 commits into
masterfrom
fix_nested_scroll
Apr 24, 2026
Merged

fix(scroll): correctly calculate absolute Y for nested regions#126
jacob314 merged 6 commits into
masterfrom
fix_nested_scroll

Conversation

@jacob314

@jacob314 jacob314 commented Apr 22, 2026

Copy link
Copy Markdown
Owner

This PR resolves an issue where scrolling a nested child component would trigger excessive repaints of the entire screen.

Cause:
Previously, TerminalBufferWorker calculated the absolute Y coordinate (absY) of nested scrollable regions by making an incorrect assumption that all regions were positioned directly relative to the root camera.

Solution:
The fix introduces a recursive processRegionForScroll function that correctly accumulates scrollTop and scrollLeft offsets from all ancestor regions, ensuring that absX and absY are accurately computed for deeply nested scrollable children. This effectively eliminates the excessive screen repainting bug.

Additional Changes:

  • Added a robust performance test (test/repro-nested-scroll-repaint.test.tsx) to guarantee the number of repainted lines remains within the expected bound (<= 6), preventing future regressions.
  • Refactored examples/sticky/sticky.tsx to remove code duplication around inner scrolling states, consolidating them into a single state record.

@jacob314 jacob314 changed the title Fix scroll of a nested child excessive update bug. fix(scroll): correctly calculate absolute Y for nested regions Apr 22, 2026
@jacob314 jacob314 force-pushed the fix_nested_scroll branch 3 times, most recently from c06d007 to 36717e0 Compare April 22, 2026 21:44
@jacob314

Copy link
Copy Markdown
Owner Author

I've reviewed the PR and applied some fixes locally instead to clean things up:

  1. Removed Debugging Artifact: Removed the debugRainbow: true flag from examples/sticky/index.ts which appeared to be accidentally left in.
  2. Optimized src/render-sticky.ts: Stored the original index when adding to activeStickyNodes to avoid an O(N^2) indexOf(find(...)) lookup.
  3. Deduplicated src/render-sticky.ts: Extracted the logic for finding the nextStickyNodeInfo into a findNextStickyNode helper function and replaced the duplicated loops.

(Note: The sticky-backbuffer-overlap test was failing locally for me, but as noted in the repo context, tests that use node-pty or similar terminal emulators can be flaky/failing locally on macOS, so I've left the test logic alone as instructed.)

Otherwise, the structural changes and fixes in this PR look good!

@jacob314 jacob314 force-pushed the fix_nested_scroll branch 2 times, most recently from 386ac29 to 4400482 Compare April 22, 2026 23:19
more fixes.

refactor(sticky): optimize sticky node lookup and remove debug flag

- Removed `debugRainbow: true` from `examples/sticky/index.ts` which was an artifact.
- Refactored `identifyActiveStickyNodes` in `src/render-sticky.ts` to use `originalIndex` during population, eliminating the O(N^2) `indexOf(find(...))` lookup.
- Extracted next sticky node discovery into a `findNextStickyNode` helper to reduce duplication.
@jacob314 jacob314 force-pushed the fix_nested_scroll branch from 4400482 to 842c373 Compare April 23, 2026 04:03
@jacob314 jacob314 force-pushed the fix_nested_scroll branch from 9a20d85 to e5b5329 Compare April 23, 2026 20:08
Comment thread examples/sticky/index.ts
rows,
}),
{
debugRainbow: true,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intentionally added for easier debugging that this sample doesn't introduce inefficient rendering.

}
}, [recordFilename, startRecording]);

const [innerStates, setInnerStates] = useState<

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tweaks to this sample to reproduce an issue noticed in Gemini CLI

Comment thread src/worker/render-worker.ts Outdated
});

for (const region of this.sceneManager.regions.values()) {
const processRegionForScroll = (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defining this recursive helper as a closure inside the render() method—which is called on every frame results in unnecessary object allocations and increases garbage collection pressure. Maybe refactor this into a private class method?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed. done.

Comment thread src/worker/render-worker.ts Outdated
const region = this.sceneManager.getRegion(node.id);
if (!region) return;

const absX = Math.round(region.x + offsetX);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be safer to accumulate the offsets as floats and only apply Math.round() at the final step before usage?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Done.

@jacob314 jacob314 merged commit d9685a7 into master Apr 24, 2026
1 check passed
@jacob314 jacob314 deleted the fix_nested_scroll branch April 24, 2026 22:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants