πŸ“„ 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)

  • HorizontalScrollWindow
  • calculateHorizontalScrollWindow

πŸ–₯️ 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,
  }
}