Haiku BView PushState and PopState: Isolate Drawing Helpers
Bracket temporary BView drawing changes with balanced state pushes, use scope-bound cleanup, and understand what the Interface Kit state stack does not restore.
A BView drawing helper that changes color, mode, pen settings, or coordinates can leave the next helper with a different rendering context than it expected. This kind of state leak is especially easy to introduce when drawing routines are composed: a badge, focus ring, selection overlay, or diagnostic marker may work alone and change the appearance of later output.
Haiku’s BView API provides PushState() and PopState() for a balanced state stack. The public header exposes both methods, and the Interface Kit implementation sends matching state-stack messages to app_server. Use them to isolate temporary drawing configuration in a narrow scope. They are not a framebuffer transaction, a model rollback, or permission to draw to a view from an arbitrary thread.
A scoped guard keeps the pair together
For a helper with early returns or multiple drawing calls, wrap the pair in a small non-copyable guard. Its lifetime should be entirely inside a valid BView operation, such as Draw() while the owning window’s drawing context is active:
#include <Rect.h>
#include <View.h>
class ScopedViewState {
public:
explicit ScopedViewState(BView& view)
: fView(view)
{
fView.PushState();
}
~ScopedViewState()
{
fView.PopState();
}
ScopedViewState(const ScopedViewState&) = delete;
ScopedViewState& operator=(const ScopedViewState&) = delete;
private:
BView& fView;
};
void BadgeView::Draw(BRect updateRect)
{
const BRect badge(8, 8, 95, 31);
if (badge.right < updateRect.left || badge.left > updateRect.right
|| badge.bottom < updateRect.top || badge.top > updateRect.bottom) {
return;
}
ScopedViewState state(*this);
SetDrawingMode(B_OP_OVER);
SetHighColor(24, 104, 176);
FillRect(badge);
}
The rectangle comparison is only a cheap rejection test: edge contact may still draw, which is harmless. The normal view clipping rules remain responsible for limiting output to the update region. The local guard pushes after the no-op path and pops as the function exits. If a helper has several returns after the push, the destructor still balances the state transition.
This pattern assumes the view remains alive and its owner context is valid until the guard is destroyed. Use it within normal looper/drawing serialization; do not retain the guard, pass a BView to a worker thread, or let a drawing helper outlive its view. A helper that changes no drawing state does not need a push/pop pair.
What the stack protects - and its limits
The public method names are an API-level signal to save and restore view drawing state. In the pinned Haiku implementation, PushState() sends an app_server push message and begins a new state frame; PopState() sends the corresponding pop and flushes when it is outside a view transaction. The app_server implementation maintains a stack of draw-state objects. The client also invalidates cached state on pop, so later getters may need to ask the server for a value again.
Treat this as isolation for graphics context, not as a transaction over pixels. Popping does not erase pixels already drawn, restore your model data, undo an Invalidate(), or redraw previous content. If a helper paints the wrong pixels before returning, state restoration will not repair the image. Correct redraw still depends on storing application state and reconstructing invalidated regions in Draw().
PushState() also initializes a new coordinate frame in the current implementation: the new frame begins with origin at zero, scale one, and a reset transform. That is a practical reason not to assume that a helper can push and continue using every transformed coordinate established by its caller. Pass geometry explicitly in the coordinate system the helper expects, or establish its required transform after the push.
The stack must remain balanced. BView’s own drawing path uses a push/draw/pop sequence around the Draw() hook. An unmatched push or pop in application drawing can therefore corrupt the expected nesting and affect later rendering. Use a guard instead of manually pairing calls across branches, and avoid returning from a lower-level helper while leaving a caller’s push outstanding. The public methods return void, so the API does not offer a status value for application code to recover from an internal state-allocation failure; the guard keeps the normal application control-flow pair together, not an allocation transaction.
Coordinate the state boundary with redraw design
State restoration does not replace update clipping. Draw(BRect updateRect) is asked to reconstruct output for an update region, while the view system clips drawing to the appropriate region. A drawing helper may skip work that cannot intersect updateRect, but it should not assume that the pixels it drew on a prior call remain visible. The model, not the display surface, remains authoritative.
A useful design is for a view’s Draw() method to set a stable baseline, then call small helpers that each own their temporary state. Avoid an implicit convention such as “the caller has already selected blue and B_OP_OVER” unless that requirement is part of a documented helper contract. Either make the required color and mode explicit at the helper boundary or save state before temporarily changing them.
Keep PushState()/PopState() inside the view’s normal owner-lock and drawing context. The implementation checks the owner lock and switches the current view context when needed, but this is not a general thread-safety mechanism. A worker should publish model changes through the window’s normal synchronization path and request invalidation; it should not issue drawing commands against a BView simply because it can hold a pointer.
When debugging state leakage, compare a helper’s output when called alone and between two known drawing operations. Temporarily log its entry assumptions, remove state pushes one at a time, and verify nesting on every exit path. Also test a full invalidation after the sequence: that distinguishes a stale-pixel problem from a state-stack problem. A test that only captures the first paint can miss both.
Use the stack for what it promises: a balanced drawing-context boundary around local configuration. Keep data mutation, redraw scheduling, clipping policy, and thread ownership explicit. That division makes composed view code predictable without treating app_server’s state stack as an undo system.
Related:
- Haiku BView Invalidation: Reconstructing Dirty Regions Without Flicker
- Haiku BShape: Constructing Reusable Vector Paths
Sources:
- Haiku public BView header at commit c74d249db11e0579ad6d784b03bc4a7685980444
- Haiku BView implementation at commit c74d249db11e0579ad6d784b03bc4a7685980444
- Haiku app_server View implementation at commit c74d249db11e0579ad6d784b03bc4a7685980444
- Haiku app_server DrawState implementation at commit c74d249db11e0579ad6d784b03bc4a7685980444