π File detail
utils/horizontalScroll.ts
π§© .tsπ 138 linesπΎ 4,302 bytesπ text
β Back to All Filesπ― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes HorizontalScrollWindow and calculateHorizontalScrollWindow β mainly functions, hooks, or classes.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
export type HorizontalScrollWindow = { startIndex: number endIndex: number showLeftArrow: boolean showRightArrow: boolean
π€ Exports (heuristic)
HorizontalScrollWindowcalculateHorizontalScrollWindow
π₯οΈ Source preview
export type HorizontalScrollWindow = {
startIndex: number
endIndex: number
showLeftArrow: boolean
showRightArrow: boolean
}
/**
* Calculate the visible window of items that fit within available width,
* ensuring the selected item is always visible. Uses edge-based scrolling:
* the window only scrolls when the selected item would be outside the visible
* range, and positions the selected item at the edge (not centered).
*
* @param itemWidths - Array of item widths (each width should include separator if applicable)
* @param availableWidth - Total available width for items
* @param arrowWidth - Width of scroll indicator arrow (including space)
* @param selectedIdx - Index of selected item (must stay visible)
* @param firstItemHasSeparator - Whether first item's width includes a separator that should be ignored
* @returns Visible window bounds and whether to show scroll arrows
*/
export function calculateHorizontalScrollWindow(
itemWidths: number[],
availableWidth: number,
arrowWidth: number,
selectedIdx: number,
firstItemHasSeparator = true,
): HorizontalScrollWindow {
const totalItems = itemWidths.length
if (totalItems === 0) {
return {
startIndex: 0,
endIndex: 0,
showLeftArrow: false,
showRightArrow: false,
}
}
// Clamp selectedIdx to valid range
const clampedSelected = Math.max(0, Math.min(selectedIdx, totalItems - 1))
// If all items fit, show them all
const totalWidth = itemWidths.reduce((sum, w) => sum + w, 0)
if (totalWidth <= availableWidth) {
return {
startIndex: 0,
endIndex: totalItems,
showLeftArrow: false,
showRightArrow: false,
}
}
// Calculate cumulative widths for efficient range calculations
const cumulativeWidths: number[] = [0]
for (let i = 0; i < totalItems; i++) {
cumulativeWidths.push(cumulativeWidths[i]! + itemWidths[i]!)
}
// Helper to get width of range [start, end)
function rangeWidth(start: number, end: number): number {
const baseWidth = cumulativeWidths[end]! - cumulativeWidths[start]!
// When starting after index 0 and first item has separator baked in,
// subtract 1 because we don't render leading separator on first visible item
if (firstItemHasSeparator && start > 0) {
return baseWidth - 1
}
return baseWidth
}
// Calculate effective available width based on whether we'll show arrows
function getEffectiveWidth(start: number, end: number): number {
let width = availableWidth
if (start > 0) width -= arrowWidth // left arrow
if (end < totalItems) width -= arrowWidth // right arrow
return width
}
// Edge-based scrolling: Start from the beginning and only scroll when necessary
// First, calculate how many items fit starting from index 0
let startIndex = 0
let endIndex = 1
// Expand from start as much as possible
while (
endIndex < totalItems &&
rangeWidth(startIndex, endIndex + 1) <=
getEffectiveWidth(startIndex, endIndex + 1)
) {
endIndex++
}
// If selected is within visible range, we're done
if (clampedSelected >= startIndex && clampedSelected < endIndex) {
return {
startIndex,
endIndex,
showLeftArrow: startIndex > 0,
showRightArrow: endIndex < totalItems,
}
}
// Selected is outside visible range - need to scroll
if (clampedSelected >= endIndex) {
// Selected is to the right - scroll so selected is at the right edge
endIndex = clampedSelected + 1
startIndex = clampedSelected
// Expand left as much as possible (selected stays at right edge)
while (
startIndex > 0 &&
rangeWidth(startIndex - 1, endIndex) <=
getEffectiveWidth(startIndex - 1, endIndex)
) {
startIndex--
}
} else {
// Selected is to the left - scroll so selected is at the left edge
startIndex = clampedSelected
endIndex = clampedSelected + 1
// Expand right as much as possible (selected stays at left edge)
while (
endIndex < totalItems &&
rangeWidth(startIndex, endIndex + 1) <=
getEffectiveWidth(startIndex, endIndex + 1)
) {
endIndex++
}
}
return {
startIndex,
endIndex,
showLeftArrow: startIndex > 0,
showRightArrow: endIndex < totalItems,
}
}