Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/sticky/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const instance = render(
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.

renderProcess: true,
terminalBuffer: true,
alternateBuffer: false,
Expand Down
63 changes: 53 additions & 10 deletions examples/sticky/sticky.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ function ScrollableContent({
}
}, [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

Record<number, {scrollTop: number; isStatic: boolean}>
>({});

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const boxWidth = columns;
const contentWidth = showBorder
Expand All @@ -205,17 +209,22 @@ function ScrollableContent({

const staticContent = useMemo(() => {
const elements = [];
for (let i = 0; i < listItems.length; i += 20) {
for (let i = 0; i < listItems.length; i += 4) {
const headerIndex = i;
const headerId = headerIndex / 20;
const headerId = headerIndex / 4;
const headerText = `Sticky Header ${headerId}`;
const stickyHeaderText = `Sticky Header ${headerId} (sticky top)`;
const stickyFooterText = `Sticky Footer ${headerId} (sticky bottom)`;

const itemsInGroup = listItems.slice(headerIndex, headerIndex + 10);
const nextItems = listItems.slice(headerIndex + 10, headerIndex + 20);

const itemsInGroup = listItems.slice(headerIndex, headerIndex + 2);
const nextItems = listItems.slice(headerIndex + 2, headerIndex + 4);
if (headerId % 3 === 0) {
const innerScrollTop =
innerStates[headerId]?.scrollTop ??
(headerId === 0 || headerId === 3 ? 0 : 40);
const useStaticForInner =
useStatic && (innerStates[headerId]?.isStatic ?? true);

const innerBox = (
<Box
key={`inner-scroll-${headerId}`}
Expand All @@ -224,7 +233,7 @@ function ScrollableContent({
overflowY="scroll"
borderStyle="single"
borderColor="cyan"
scrollTop={40}
scrollTop={innerScrollTop}
>
<Box flexShrink={0} flexDirection="column" overflow="hidden">
<Box
Expand Down Expand Up @@ -260,11 +269,11 @@ function ScrollableContent({
);

elements.push(
useStatic ? (
useStaticForInner ? (
<StaticRender
key={`static-inner-scroll-${headerId}`}
width={contentWidth}
deps={[innerBox]}
deps={[innerBox, innerScrollTop]}
>
{() => innerBox}
</StaticRender>
Expand Down Expand Up @@ -395,12 +404,45 @@ function ScrollableContent({
);

return content;
}, [contentWidth, useStatic, listItems]);
}, [contentWidth, useStatic, listItems, innerStates]);

useInput((input, key) => {
const handleInnerScroll = (id: number, delta: number) => {
setInnerStates(previous => {
const current = previous[id] || {scrollTop: 0, isStatic: true};
return {
...previous,
[id]: {
isStatic: false,
scrollTop: Math.max(0, current.scrollTop + delta),
},
};
});
};

if (input === '1') {
handleInnerScroll(0, -1);
return;
}

if (input === '2') {
handleInnerScroll(0, 1);
return;
}

if (input === '3') {
handleInnerScroll(3, -1);
return;
}

if (input === '4') {
handleInnerScroll(3, 1);
return;
}

if (input === ' ') {
setListItems(previous => {
const newItems = Array.from({length: 20}).map((_, i) => ({
const newItems = Array.from({length: 4}).map((_, i) => ({
id: Date.now() + previous.length + i,
text: `Line ${previous.length + i} - ${'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(
((previous.length + i) * 5) % 6,
Expand Down Expand Up @@ -530,6 +572,7 @@ function ScrollableContent({
Press up/down arrow or w/s to scroll vertically (w/s for 30
lines, Shift for 10).
</Text>
<Text>Press 1/2 to scroll first inner view, 3/4 for second.</Text>
<Text>Press 'space' to add a block, 'c' to clear list.</Text>
<Text>
Press 'b' to toggle scrollbar ({showScrollbar ? 'on' : 'off'}),
Expand Down
1 change: 1 addition & 0 deletions src/ink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ export default class Ink {
selectionStyle: this.options.selectionStyle,
skipScrollbars: Boolean(this.terminalBuffer),
terminalBuffer: Boolean(this.terminalBuffer),
stickyHeadersInBackbuffer: this.optionsState.stickyHeadersInBackbuffer,
});

if (this.terminalBuffer && root) {
Expand Down
8 changes: 8 additions & 0 deletions src/render-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
identifyActiveStickyNodes,
renderActiveStickyNodes,
type StickyNodeInfo,
type ResolvedStickyHeaderInfo,
} from './render-sticky.js';

export function handleContainerNode(
Expand All @@ -38,6 +39,7 @@ export function handleContainerNode(
skipStaticElements: boolean;
isStickyRender: boolean;
skipStickyHeaders: boolean;
stickyHeadersInBackbuffer?: boolean;
selectionMap?: Map<DOMNode, {start: number; end: number}>;
selectionStyle?: (line: StyledLine, index: number) => void;
absoluteOffsetX: number;
Expand All @@ -54,6 +56,7 @@ export function handleContainerNode(
skipStaticElements,
isStickyRender,
skipStickyHeaders,
stickyHeadersInBackbuffer,
selectionMap,
selectionStyle,
absoluteOffsetX,
Expand All @@ -73,6 +76,7 @@ export function handleContainerNode(
nextStickyNodeInfo?: StickyNodeInfo;
cached?: StickyHeader;
anchor?: DOMElement;
resolvedInfo: ResolvedStickyHeaderInfo;
}> = [];

let verticallyScrollable = false;
Expand Down Expand Up @@ -106,6 +110,7 @@ export function handleContainerNode(
node,
scrollTop,
viewportBottom,
stickyHeadersInBackbuffer,
),
);
}
Expand Down Expand Up @@ -203,6 +208,7 @@ export function handleContainerNode(
skipStaticElements,
isStickyRender,
skipStickyHeaders: false,
stickyHeadersInBackbuffer,
selectionMap,
selectionStyle,
trackSelection,
Expand All @@ -217,6 +223,7 @@ export function handleContainerNode(
selectionMap,
selectionStyle,
trackSelection,
stickyHeadersInBackbuffer,
});

output.endChildRegion();
Expand Down Expand Up @@ -247,6 +254,7 @@ export function handleContainerNode(
skipStaticElements,
isStickyRender,
skipStickyHeaders,
stickyHeadersInBackbuffer,
selectionMap,
selectionStyle,
trackSelection,
Expand Down
6 changes: 6 additions & 0 deletions src/render-node-to-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const getOrRenderCachedRegion = (
skipStaticElements: boolean;
isStickyRender: boolean;
skipStickyHeaders: boolean;
stickyHeadersInBackbuffer?: boolean;
},
) => {
const {
Expand All @@ -87,6 +88,7 @@ const getOrRenderCachedRegion = (
skipStaticElements,
isStickyRender,
skipStickyHeaders,
stickyHeadersInBackbuffer,
} = options;
const cachedRegion = getCachedRegion(node);

Expand Down Expand Up @@ -114,6 +116,7 @@ const getOrRenderCachedRegion = (
skipStaticElements,
isStickyRender,
skipStickyHeaders,
stickyHeadersInBackbuffer,
absoluteOffsetX: 0,
absoluteOffsetY: 0,
});
Expand Down Expand Up @@ -264,6 +267,7 @@ function renderNodeToOutput(
skipStaticElements: boolean;
isStickyRender?: boolean;
skipStickyHeaders?: boolean;
stickyHeadersInBackbuffer?: boolean;
selectionMap?: Map<DOMNode, {start: number; end: number}>;
selectionStyle?: (line: StyledLine, index: number) => void;
trackSelection?: boolean;
Expand Down Expand Up @@ -373,6 +377,7 @@ function renderNodeToOutput(
skipStaticElements,
isStickyRender,
skipStickyHeaders,
stickyHeadersInBackbuffer: options.stickyHeadersInBackbuffer,
});

output.addRegionTree(cachedRegion, x, y);
Expand Down Expand Up @@ -400,6 +405,7 @@ function renderNodeToOutput(
skipStaticElements,
isStickyRender,
skipStickyHeaders,
stickyHeadersInBackbuffer: options.stickyHeadersInBackbuffer,
selectionMap,
selectionStyle,
absoluteOffsetX: absX,
Expand Down
Loading
Loading