跳到主要内容

组件约 9 分钟

滚动

overflow: scroll 容器获得原生滚动物理。哪些约束最容易踩坑,以及如何让多个窗格逐像素同步。

带有 overflow: "scroll" 的容器会变成原生可滚动的。GPUI 会自动处理滚动物理、裁剪与偏移保持。

普通的滚动容器仍然会构建每一个子元素。当集合可能变得很大时,请使用 <virtual-list>。

function Expandable({
  preview,
  children,
}: {
  preview: React.ReactNode
  children: React.ReactNode
}) {
  const [open, setOpen] = useState(false)
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
      {open ? children : preview}
      {!open && <div onClick={() => setOpen(true)}>Show more</div>}
    </div>
  )
}

基本滚动容器

function ScrollableList() {
  return (
    <div style={{ height: 300, overflow: 'scroll' }}>
      {items.map((item, i) => (
        <div key={i} style={{ height: 60, padding: 12 }}>
          {item.name}
        </div>
      ))}
    </div>
  )
}

按轴滚动:使用 overflowX: "scroll" 或 overflowY: "scroll"。overflow: "scroll" 会像浏览器一样,从一个对角线的手势同时滚动两个轴。

flex 列会把它的子元素拉伸到交叉轴,因此一个双轴容器需要它的每一行声明一个宽度。否则在 X 方向上就没有任何可平移的内容:

<div style={{ width: 260, height: 220, overflow: 'scroll', display: 'flex', flexDirection: 'column' }}>
  {rows.map((row) => (
    <div key={row.id} style={{ display: 'flex', width: 810, flexShrink: 0 }}>
      {row.cells}
    </div>
  ))}
</div>

必须一起移动的面板

原生滚动容器无法驱动冻结表头。GPUI 在滚轮那一帧移动容器,而要移动表头的 JavaScript 回调会晚一帧才到达,因此快速平移时表头会被撕裂开。

当两个窗格必须逐像素锁定时,在 React 中拥有这个偏移量:在一个不滚动的父元素上放一个 onScroll 监听器,把 scrollX 和 scrollY 保留在 state 中,并用绝对定位的包装层平移每个窗格的内容。Zed 也是这么做的:编辑器拥有自己的滚动位置,并据此绘制标尺和文本。

function Pane({ offsetX, children }: { offsetX: number; children: React.ReactNode }) {
  return (
    <div style={{ flexGrow: 1, minWidth: 0, overflow: 'hidden', position: 'relative' }}>
      {/* An empty positioned box still takes hits, so opt it out. */}
      <div style={{ position: 'absolute', left: -offsetX, top: 0, pointerEvents: 'none' }}>
        {children}
      </div>
    </div>
  )
}

把正在移动的子树放在一个 memo 组件中,其 props 在平移过程中保持不变。这样滚轮只会带来少量的样式 mutation,而不是每行一个。timeline 示例 就对一个标尺、一条轨道列以及一个片段网格做了这样的处理。

编程式滚动

请使用宿主 ref。scrollIntoView() 会向上找到最近的滚动父元素。在你已经持有一个 id 时,scrollTo 和 scrollToItem 仍然存在于 renderer 上。

function ProgrammaticScroll() {
  const lastItem = useRef<PublicInstance>(null)

  return (
    <>
      <div style={{ height: 200, overflow: 'scroll' }}>
        {items.map((item, i) => (
          <div key={i} ref={i === items.length - 1 ? lastItem : undefined}>
            {item}
          </div>
        ))}
      </div>
      <div onClick={() => lastItem.current?.scrollIntoView?.()}>Jump to last</div>
    </>
  )
}
renderer.scrollTo(elementId, x, y)        // 直接设置偏移
renderer.scrollToItem(elementId, index)   // 将子元素滚动到可见区域
renderer.scrollIntoView(elementId)        // 最近的滚动父元素
renderer.getScrollOffset(elementId)       // 返回 [x, y] 或 null