Haiku BScrollView: Viewport Geometry, Scrollbar Ranges, and Target Ownership
Understand Haiku BScrollView target geometry, scrollbar ranges, layout-aware behavior, and safe replacement patterns for responsive native interfaces.
BScrollView is a container that connects a target BView to one or two BScrollBar objects. Its value is not just that it draws a border around a large view: it owns the relationships among the viewport, target, and bars as the enclosing window is laid out and resized. Correct use depends on knowing which view owns content geometry, which object owns the bars, and whether the target expects the scroll view to calculate scrollbar ranges or manages those ranges itself.
This is a focused companion to Haiku’s broader layout API. It covers the scroll container’s target and viewport semantics rather than general BLayout construction or drawing invalidation. The official documentation calls the target and bars siblings under the scroll view, and the current implementation fills in important operational details: requested bars are created explicitly, layout-aware targets have a distinct range-management path, and replacing a target detaches but does not delete the old view.
The view tree and coordinate spaces
A BScrollView is the parent container. The target is one child, and each requested scrollbar is another child. The target’s bounds describe its content coordinate space; the scroll view’s inner frame is the visible viewport after accounting for border and scrollbar geometry. Scrolling changes the portion of the target coordinate space displayed through that viewport. It does not automatically make arbitrary content smaller, virtualized, or lazily loaded.
That distinction prevents a common layout error: placing the content view directly in a parent and then adding a separate scrollbar beside it, while expecting BScrollView to coordinate the two. Use the target relationship so BScrollView can wire each bar to the target and resize/position them as a unit. The target can then receive scrolling through the normal view/scrollbar mechanism instead of a second, conflicting set of geometry calculations in the window.
The constructor lets the caller request a horizontal bar, a vertical bar, both, or neither. These booleans determine whether a bar object exists; do not assume the container will create an omitted bar later just because content overflows. ScrollBar(B_HORIZONTAL) or ScrollBar(B_VERTICAL) returns the requested bar, and can return NULL when that direction was not configured. Check before customizing it.
BView* content = new ResultsView("results");
BScrollView* scroller = new BScrollView("results-scroller", content,
0, false, true, B_FANCY_BORDER);
// The layout-aware constructor above requests a vertical bar only.
// ScrollBar(B_HORIZONTAL) would return NULL for this configuration.
BScrollBar* vertical = scroller->ScrollBar(B_VERTICAL);
if (vertical != NULL)
vertical->SetSteps(16.0f, 160.0f);
The example uses the layout-aware constructor overload: after the target argument, it accepts view flags, horizontal and vertical booleans, and a border style. The older overload also accepts a resizingMode argument. Do not accidentally shift arguments between these overloads; a boolean in the wrong position can silently express a different resize policy or fail to compile. Pick the overload that matches how the parent manages layout, and verify the signature in the headers for the Haiku revision being targeted.
Let the container own the viewport calculation
The layout-aware overload is intended for use in a BLayout. BScrollView participates in preferred-size calculation and incorporates the target’s preferred size, requested bars, and border into its own sizing behavior. The scroll view should be placed in the parent layout like any other child; avoid manually moving its bars or setting target frames from a window resize handler while a layout is also controlling those frames.
In the current implementation, when the target supports layout but is not marked B_SCROLL_VIEW_AWARE, BScrollView::FrameResized() uses the target’s preferred size and the current viewport bounds to update each existing bar’s range, step sizes, and proportion. The range represents the amount by which the target extends beyond the visible area on that axis, clamped to zero when it fits. The proportion communicates the visible share of content. This lets a standard layout-aware target use the container’s sizing calculation without reimplementing it.
That implementation condition is specific: the target must support layout, and it must not opt into the B_SCROLL_VIEW_AWARE behavior. If you mark a custom target as scroll-view-aware, the scroll view deliberately does not perform that automatic range setup for it. Your target then needs to coordinate its own content dimensions and scrollbar values/ranges. Do not set that flag merely to suppress a visual quirk; it transfers responsibility for a working scroll model to the target.
For a custom large canvas or data view, define one authoritative content size and derive both axes from it. Let viewportWidth and viewportHeight come from the actual inner viewport, not the outer BScrollView frame. For each requested axis, the maximum scroll offset is max(0, contentExtent - viewportExtent). Set the range from zero to that maximum, set a proportion consistent with the visible fraction, and choose small/large steps that match the user’s unit of work. Recompute when content, font metrics, zoom, or viewport size changes. Avoid updating the range from two callbacks with different stale dimensions.
Distinguish scrolling from changing content size
BScrollBar::SetRange() controls the bar’s representable value interval; it does not resize the target. SetProportion() affects the thumb size relative to the content area. SetSteps() defines the small and large increments used for directional and page-style movement. A range of zero means there is no scroll offset to represent on that axis, even if the bar object was requested. Keep all three values consistent with the target’s actual content model.
A common bug is to set the range to the full content width or height instead of the overflow beyond the viewport. That lets the thumb scroll past the last visible content. Another is to treat BScrollView::Bounds().Width() as interchangeable with the visible content width in every configuration. Border and scrollbar dimensions affect the inner frame, and the implementation subtracts bar sizes when it computes target placement. Prefer the container and layout APIs over hard-coded pixel subtraction. If a custom target is scroll-aware, base calculations on the actual viewport available to that target and test with each requested bar combination.
The scroll bar moves its target through the standard scroll mechanism, but the target still owns its drawing and content. Keep Draw() bounded to the update region and make rendering a function of the current visible coordinates. Do not draw the entire document into an off-screen bitmap just to make scrolling work unless that is an explicit design tradeoff; BScrollView is a viewport, not a content virtualization engine.
Target replacement is detach, not destruction
SetTarget() updates the stored target, points existing bars at the new target, adjusts the new view’s frame, informs it that it is targeted by this scroll view, and adds it as a child. When replacing an existing target, the current implementation first calls TargetedByScrollView(NULL) and removes the old view. Its source explicitly notes that the old view is not supposed to be deleted by BScrollView.
This is an ownership boundary. If the application replaces content dynamically, it remains responsible for the lifetime of the detached view. Keep a pointer if it will be reused, or delete it only after ensuring no layout, callback, or message still references it. Do not assume SetTarget(newView) frees old content. Conversely, do not delete a view while it is still the scroll view’s active target. In a window with a looper, perform replacement on the window’s message thread or under the proper window-lock discipline so the child tree is not modified concurrently with drawing or event delivery.
void ResultsPane::ReplaceContent(BView* replacement)
{
BView* previous = fScrollView->Target();
fScrollView->SetTarget(replacement);
// SetTarget detaches but does not delete the previous view.
// Retain it for reuse or dispose of it here if ownership permits.
if (previous != NULL)
delete previous;
}
The deletion is safe only if ResultsPane owns the old target and no other object uses it. If that condition is not guaranteed, store it, transfer ownership explicitly, or arrange deferred disposal after active callbacks complete. This example is a lifecycle pattern, not a license to delete arbitrary views returned by Target().
Layout and resize failure modes
If the target has a fixed frame but the scroll view is managed by a layout, the target may not report the preferred size that the scrollbar calculation expects. The range can then be zero or too small even though content is clipped. If the target’s preferred size changes after construction, ensure the layout is invalidated and the target’s preferred-size contract reflects the new content. If the custom target uses B_SCROLL_VIEW_AWARE, update both bars whenever the content or viewport changes.
If a scrollbar is NULL, check the constructor arguments before trying to configure it. If scrolling works in one direction but not the other, inspect the target’s frame and preferred size on that axis, confirm that the corresponding bar was requested, and verify that no later code resets its range. If the thumb size is nonsensical, compare SetProportion() with viewport-to-content ratio and inspect whether the target and the scroll view are using the same units.
Borders matter too. B_NO_BORDER, B_PLAIN_BORDER, and B_FANCY_BORDER change the frame available to the target, and the implementation aligns bars with the inner frame. Avoid manually adding the scrollbar thickness to a layout constraint unless you intentionally want that extra space: the scroll view’s preferred-size behavior already accounts for its composition. When switching border styles, re-test minimum and preferred sizes rather than assuming the target remains the same size.
Accessibility and interaction details
Choose scroll steps that correspond to content semantics. For text, a small step near a line height and a large step near a page can be more predictable than a one-pixel movement. For a canvas, steps can correspond to a meaningful grid or viewport fraction. These values are hints to scrollbar interaction; they do not define a universal keyboard or mouse policy for every target. Test keyboard focus, wheel/trackpad input where supported, and the bar’s visible thumb after window resize.
Do not add a second competing scroll mechanism without a reason. A target that independently intercepts wheel events and manually updates the same scrollbar can double-scroll or fight with the bar’s target behavior. If a custom view must implement special navigation, centralize the offset in one model and keep bar values synchronized from that model. Ensure content remains reachable at both ends and that resizing preserves a meaningful location rather than jumping unpredictably.
Verify geometry with a small matrix
Exercise the target with horizontal-only, vertical-only, both-bars, and no-bar configurations. Test content smaller than the viewport, exactly fitting it, and larger on one or both axes. Resize the window repeatedly and check that the range reaches the final content edge, the proportion reflects the visible share, and the target does not overlap the border or bars. Replace the target while the view is attached, then confirm the old view is detached and disposed of only by its owner.
For a layout-aware ordinary target, verify its preferred size drives the automatic range path. For a B_SCROLL_VIEW_AWARE target, verify your own resize/content-change code updates range, proportion, and steps. Record the Haiku revision and the target flags in bug reports: a change in layout participation can explain why the same BScrollView has switched from automatic range setup to application-managed behavior.
The central rule is to assign one owner to each piece of geometry. Let the scroll view place its children; let the target define content extent; and either let the current layout-aware default calculate bar ranges or deliberately make the target responsible. Once those boundaries are explicit, resizing and dynamic content replacement become predictable instead of a collection of compensating pixel adjustments.
Related:
- How to Build Resizable Haiku Interfaces with BLayout Instead of Fixed Coordinates
- Haiku BView Invalidation: Reconstructing Dirty Regions Without Flicker
Sources: