ScrollView 滚动容器
ScrollView 是可滚动容器。它不会改变子组件的声明方式:你仍然用普通 View、Text、Border 组织内容,只是这些内容会先渲染到一个更大的内部缓冲区,再按当前 offset 显示到可见视口。

这个录屏由 docs/tapes/scrollview.tape 生成。示例展示一个受控文档阅读器:左侧是 ScrollView,右侧实时显示 ScrollViewState 的 offset。按 j/k 逐行滚动,按 PageDown/PageUp 翻页,状态面板会跟着变化。
按 j/k 或方向键逐行滚动,按 PageDown/PageUp 翻页,按 Home/End 跳到顶部或底部,按 q 退出。
最小形态不需要自己保存滚动状态:
不传 state 时,ScrollView 内部会创建自己的 ScrollViewState,并在当前 input layer 注册滚动事件。它支持 j/k、方向键、PageUp/PageDown、Home/End 和鼠标滚轮。
内置滚动只由 active(默认 true)门控,与 state 正交:传外部 state 不会关掉内置滚动——你可以既拿到句柄、又保留内置按键(与其它选择类组件一致)。内部 handler 现在对真正处理了的滚动键返回 EventResult::Consumed(其余返回 Ignored),所以同层无关快捷键(页面 q 退出、? 帮助)照常运行。
当页面需要读取 offset、重置滚动位置,或把滚动状态和其它 UI 联动时,传入外部 state:
示例用的就是外部状态:j/k 移动一个选中光标,视口通过 scroll_state.write().scroll_to_index(selected) 自动跟随;状态面板读取 scroll_state.read().offset()。
ScrollViewState 还暴露 size() / page_size()、is_at_bottom()(日志/聊天的“贴底”逻辑)、scroll_to_visible(y, height),以及选中项联动滚动的原语 scroll_to_index(i) / child_area(i)——把第 i 个直接子节点滚进视口。ScrollView 每帧记录各直接子节点在内容缓冲中的区域,故页面在一组子节点上移动选中后,只需 scroll_to_index(selected);子布局与”选中谁”无关,无需额外测量。handle_event 返回 bool(是否处理),业务 handler 可据此映射为 Consumed / Ignored。
ScrollView 的内容尺寸来自子节点的布局约束。示例里每一行都显式设置高度:
如果子节点没有稳定高度,滚动距离会变得难预测。长文档、日志、快捷键表、只读详情页这类场景,通常把每一行固定为 Constraint::Length(1) 会更容易控制。
block 会在滚动视口外绘制边框和标题:
滚动条通过 Scrollbars 控制。Automatic 只在内容超出时显示;Always 会预留稳定空间;Never 适合明确不需要某个方向滚动的场景。
滚动条盖在边框上
Section titled “滚动条盖在边框上”有 block 边框时,Scrollbars::over_border(默认 true)把滚动条画在边框环上——轨道落在右/下边框线上,内容占满整个 inner。设为 false 则把滚动条退到边框内侧,视口相应少一行/一列:
scrollview 示例支持运行时切换(按 b),可对比两种观感。无论哪种模式,偏移量都按真实可见视口裁剪,所以最后一行/列始终可达。
与输入层组合
Section titled “与输入层组合”ScrollView 的 handler 注册在 EventScope::Current。放进 Modal 或 ShortcutInfoModal 时,它会自然归属前景弹层的 current layer,所以弹窗内滚动不会穿透到背景页面。
如果你只需要展示快捷键分组,优先用 ShortcutInfoModal;如果要渲染非常长的、带选择态的列表,看 VirtualList。