Use the scroll API directly¶
Most projects use the ScrollView helper from Extra/DebugUI/
covered in Add a scroll view. The helper
calls a lower-level scroll API on UI that is also public, and a
caller can use it directly to draw custom scrollbars, apply a
different clip region, or read scroll metrics before laying out
content.
When to bypass the helper¶
Use the lower-level API when one of the following applies:
- The default scrollbar visual does not match your theme. The helper draws a fixed-style rounded handle; the lower-level API hands you the handle rect to draw yourself.
- You need to clip to a non-rectangular region (rounded corners, multiple panes), so you want to apply your own clip rect.
- You need the calculated content area before deciding what to
lay out.
CalculateScrollViewWidthandCalculateScrollViewHeightanswer that question without opening a scope.
If none of those apply, use the helper. The lower-level API is more verbose and takes more care to use correctly.
To open a scroll scope¶
- Build a
ScrollConfig. The same struct the helper uses: - Optionally calculate the content area before laying out: These return the inner area available to content after subtracting padding and scrollbar thickness. Useful when the total content size depends on the available width (text wrapping, for instance).
- Open the scope with
BeginScroll. The call returns aScrollLayoutwith the rects you need to draw the visual: - Draw the chrome you want using the layout rects:
- Push the content rect, optionally clip to the view rect, then
lay out content:
The
ui.PushRect(layout.ContentRect); ui.PushClipRect(layout.ViewRect); // content layout calls go here ui.PopClip(); ui.Pop();ui.Pop()balances theui.PushRect(layout.ContentRect)above;EndScrolldoes not pop it for you. - Close the scope with
EndScroll:EndScrollpops the rects pushed byBeginScroll, consumes mouse-wheel input if the cursor is over the view rect, and matches thePushIDissued byBeginScroll.
ScrollLayout fields¶
BeginScroll fills a ScrollLayout with seven rects in
layout space:
| Field | What it covers |
|---|---|
BackgroundRect |
The full scroll area, including padding and scrollbar gutter. Use it to draw the surface behind everything. |
ViewRect |
The visible content area after padding. Use it as the clip rect. |
ContentRect |
The content rect for the current frame, offset by the scroll position and sized to the content. Pushed by BeginScroll. |
VerticalHandleContainerRect |
The track the vertical handle slides in. |
VerticalHandleRect |
The vertical scrollbar handle. |
HorizontalHandleContainerRect |
The track the horizontal handle slides in. |
HorizontalHandleRect |
The horizontal scrollbar handle. |
When Direction is Vertical, the horizontal rects are zero-sized;
when Horizontal, the vertical rects are zero-sized. Both
populates everything; None leaves both pairs zero-sized.
ScrollConfig fields¶
| Field | Purpose |
|---|---|
Direction |
One of None, Vertical, Horizontal, Both. |
BackgroundPadding |
Pixels of padding around the view rect. |
ScrollSize |
Scrollbar thickness in pixels. |
Snapping to bottom¶
ui.ScrollToBottom("log") flags the scroll area for a snap on the
next BeginScroll call. It is safe to call from anywhere, even
while another scroll scope is open:
The snap clamps the vertical offset to
max(0, contentHeight - viewHeight). The flag is consumed when
the matching BeginScroll runs, so the call only takes effect
once.
The full pattern¶
var config = new ScrollConfig
{
Direction = ScrollDirection.Vertical,
BackgroundPadding = 5,
ScrollSize = 10,
};
int contentW = ui.CalculateScrollViewWidth(config);
int totalHeight = items.Count * itemHeight;
ui.BeginScroll("items", new Vector2Int(contentW, totalHeight),
config, out ScrollLayout layout);
ui.FillRounded(layout.BackgroundRect, backgroundColor, 10);
ui.FillRounded(layout.VerticalHandleRect, handleColor, 5);
ui.PushRect(layout.ContentRect);
ui.PushClipRect(layout.ViewRect);
{
ui.PushSeriesVertical(itemHeight);
{
for (int i = 0; i < items.Count; i++)
{
DrawItem(ui, items[i]);
if (i < items.Count - 1)
ui.Next();
}
}
ui.Pop();
}
ui.PopClip();
ui.Pop();
ui.EndScroll();
Pitfalls¶
Forgetting EndScroll.
: BeginScroll pushes rects and an ID; EndScroll pops them.
A missing EndScroll corrupts the rect and ID stacks and
triggers ValidateStacks at end of frame.
Drawing handles before BeginScroll.
: The handle rects come from the out ScrollLayout, which
BeginScroll populates. Read the layout after the call.
Mismatched clip-rect pop.
: If you PushClipRect inside the scroll scope, pair it with
PopClip before EndScroll. EndScroll does not pop the
clip stack.
The handle stays in place even though content has changed.
: The content size passed to BeginScroll is stale. Recompute
it from the underlying data each frame.
Custom scrollbar interactions don't work.
: The lower-level API draws no handle interaction itself; the
helper does. To support handle-drag and click-on-track, follow
the pattern in Source/Extra/DebugUI/ScrollView.cs or call
the helper instead.
Additional resources¶
- Add a scroll view: the high-level helper most projects use.
- Stacks: why every push needs a matching pop.
- Input and focus: the rules for mouse-wheel routing inside a scroll area.
- State: scroll position is owned by Tick UI, keyed by the scroll ID.