source dump of claude code
at main 137 lines 4.3 kB view raw
1export type HorizontalScrollWindow = { 2 startIndex: number 3 endIndex: number 4 showLeftArrow: boolean 5 showRightArrow: boolean 6} 7 8/** 9 * Calculate the visible window of items that fit within available width, 10 * ensuring the selected item is always visible. Uses edge-based scrolling: 11 * the window only scrolls when the selected item would be outside the visible 12 * range, and positions the selected item at the edge (not centered). 13 * 14 * @param itemWidths - Array of item widths (each width should include separator if applicable) 15 * @param availableWidth - Total available width for items 16 * @param arrowWidth - Width of scroll indicator arrow (including space) 17 * @param selectedIdx - Index of selected item (must stay visible) 18 * @param firstItemHasSeparator - Whether first item's width includes a separator that should be ignored 19 * @returns Visible window bounds and whether to show scroll arrows 20 */ 21export function calculateHorizontalScrollWindow( 22 itemWidths: number[], 23 availableWidth: number, 24 arrowWidth: number, 25 selectedIdx: number, 26 firstItemHasSeparator = true, 27): HorizontalScrollWindow { 28 const totalItems = itemWidths.length 29 30 if (totalItems === 0) { 31 return { 32 startIndex: 0, 33 endIndex: 0, 34 showLeftArrow: false, 35 showRightArrow: false, 36 } 37 } 38 39 // Clamp selectedIdx to valid range 40 const clampedSelected = Math.max(0, Math.min(selectedIdx, totalItems - 1)) 41 42 // If all items fit, show them all 43 const totalWidth = itemWidths.reduce((sum, w) => sum + w, 0) 44 if (totalWidth <= availableWidth) { 45 return { 46 startIndex: 0, 47 endIndex: totalItems, 48 showLeftArrow: false, 49 showRightArrow: false, 50 } 51 } 52 53 // Calculate cumulative widths for efficient range calculations 54 const cumulativeWidths: number[] = [0] 55 for (let i = 0; i < totalItems; i++) { 56 cumulativeWidths.push(cumulativeWidths[i]! + itemWidths[i]!) 57 } 58 59 // Helper to get width of range [start, end) 60 function rangeWidth(start: number, end: number): number { 61 const baseWidth = cumulativeWidths[end]! - cumulativeWidths[start]! 62 // When starting after index 0 and first item has separator baked in, 63 // subtract 1 because we don't render leading separator on first visible item 64 if (firstItemHasSeparator && start > 0) { 65 return baseWidth - 1 66 } 67 return baseWidth 68 } 69 70 // Calculate effective available width based on whether we'll show arrows 71 function getEffectiveWidth(start: number, end: number): number { 72 let width = availableWidth 73 if (start > 0) width -= arrowWidth // left arrow 74 if (end < totalItems) width -= arrowWidth // right arrow 75 return width 76 } 77 78 // Edge-based scrolling: Start from the beginning and only scroll when necessary 79 // First, calculate how many items fit starting from index 0 80 let startIndex = 0 81 let endIndex = 1 82 83 // Expand from start as much as possible 84 while ( 85 endIndex < totalItems && 86 rangeWidth(startIndex, endIndex + 1) <= 87 getEffectiveWidth(startIndex, endIndex + 1) 88 ) { 89 endIndex++ 90 } 91 92 // If selected is within visible range, we're done 93 if (clampedSelected >= startIndex && clampedSelected < endIndex) { 94 return { 95 startIndex, 96 endIndex, 97 showLeftArrow: startIndex > 0, 98 showRightArrow: endIndex < totalItems, 99 } 100 } 101 102 // Selected is outside visible range - need to scroll 103 if (clampedSelected >= endIndex) { 104 // Selected is to the right - scroll so selected is at the right edge 105 endIndex = clampedSelected + 1 106 startIndex = clampedSelected 107 108 // Expand left as much as possible (selected stays at right edge) 109 while ( 110 startIndex > 0 && 111 rangeWidth(startIndex - 1, endIndex) <= 112 getEffectiveWidth(startIndex - 1, endIndex) 113 ) { 114 startIndex-- 115 } 116 } else { 117 // Selected is to the left - scroll so selected is at the left edge 118 startIndex = clampedSelected 119 endIndex = clampedSelected + 1 120 121 // Expand right as much as possible (selected stays at left edge) 122 while ( 123 endIndex < totalItems && 124 rangeWidth(startIndex, endIndex + 1) <= 125 getEffectiveWidth(startIndex, endIndex + 1) 126 ) { 127 endIndex++ 128 } 129 } 130 131 return { 132 startIndex, 133 endIndex, 134 showLeftArrow: startIndex > 0, 135 showRightArrow: endIndex < totalItems, 136 } 137}