Haiku BCardView: Page Navigation and Shared Layout
Build Haiku multi-page interfaces with BCardView and BCardLayout, preserving page state while validating selection, sizing, focus, and navigation.
BCardView is a container for a stack of child views where one card is visible at a time. Its constructor installs a BCardLayout, available through CardLayout(). The hidden pages remain constructed, so switching cards can preserve each page’s local state and avoid reconstructing controls on every navigation step. This is useful for setup wizards, preference panes, and detail pages with a common shell.
The key tradeoff is that all pages exist together. They contribute to layout sizing, consume memory, and may need explicit lifecycle handling even when not visible. A card container is not a navigation controller: it does not validate a form, save a draft, move focus, update a breadcrumb, or define back-button semantics. Those are application responsibilities around the selected item.
Add pages and select by stable meaning
BCardView accepts no layout argument because its constructor creates a BCardLayout. The current class exposes CardLayout() to configure that layout. Add each page as a view and check the returned BLayoutItem*; then select an existing item or its current zero-based index.
#include <CardLayout.h>
#include <CardView.h>
#include <Layout.h>
#include <View.h>
BCardView* pages = new BCardView("setup-pages");
BCardLayout* layout = pages->CardLayout();
BView* accountPage = new BView("account-page", 0);
BLayoutItem* accountItem = layout->AddView(accountPage);
if (accountItem == NULL) {
// Handle failure without publishing a partially initialized wizard.
}
layout->SetVisibleItem(accountItem);
This sketch omits the page’s own layout and the application’s error cleanup. In production, construct the page, install its internal layout, then add it to the card stack. If adding fails, decide which layer owns the unattached view and delete it only after confirming it was not adopted. Do not continue with a null item. BCardView::SetLayout() accepts a BCardLayout or a derivative; another layout type is ignored by the current implementation. Prefer the constructor-provided layout unless you have a concrete specialization.
SetVisibleItem(int32) takes a zero-based index. If the index is negative or beyond the current item count, the documented behavior is to show no card and display the default gray background. Passing a null or foreign BLayoutItem* likewise selects no visible page. Guard the transition before calling the API:
bool
ShowPage(BCardView* pages, int32 index)
{
if (pages == NULL || index < 0
|| index >= pages->CardLayout()->CountItems())
return false;
pages->CardLayout()->SetVisibleItem(index);
return true;
}
Index is convenient for a small fixed stack, but it is not a durable identity. Inserting a page or removing an earlier page changes later indices. For a wizard that evolves or has conditional steps, maintain an enum/string route key in the application model and resolve it to the current item at the moment of navigation. A retained BLayoutItem* can also identify an item while the layout owns it, but do not use it after that item has been removed or the layout destroyed.
Validate navigation before changing the visible card
Treat next/back as state transitions rather than bare index increments. A “Next” action may need to validate a required field, save a draft, or wait for asynchronous verification before selecting the next card. If validation fails, leave the current item selected, focus the invalid field, and explain the error near that field. If a background check completes after the user navigated elsewhere, include a generation or page identifier so an old response cannot unexpectedly move the current card.
Keep route state outside the view tree. For a fixed wizard, model the current step and allowed transitions explicitly. For branching workflows, resolve the next step based on validated model state, not visibleIndex + 1. Back navigation should not silently discard unsaved values; decide whether pages retain values, reset on re-entry, or serialize drafts into the model. Hidden views remain alive, but that alone is not a persistence guarantee across application restart or archive reload.
When an external event requests a page change, send it to the window’s message loop or otherwise coordinate with the view’s owner thread. Updating a layout from an arbitrary worker while the window is processing drawing or input risks inconsistent UI state. Workers should return immutable results; the window looper validates that the result still applies, updates the model, and then selects a card.
Sizing the stack as a whole
The card layout keeps a fixed container size based on the layout constraints of its pages; current documentation says pages should have comparable dimensions for a visually consistent stack. A single oversized hidden page can influence the container’s minimum, maximum, or preferred size and make the outer window larger than expected. Set page constraints deliberately and test the aggregate size, especially when one step contains long localized text or a large form.
Do not design each page against a screenshot at one window size. Use responsive layouts inside the cards and account for minimum widths, font changes, and localization. A card view is only a one-page-at-a-time presentation; it does not automatically make its descendants scrollable when the available space shrinks. Add a BScrollView or another deliberate layout if content can exceed the usable area. Avoid giving every page incompatible maximum sizes and expecting the card layout to switch the outer window size on each step.
When conditional pages are added and removed, recompute or preserve the selected item’s semantic key. Removing the current item can leave the user with no visible page; choose and select a valid successor before or immediately after removal according to the operation’s ownership contract. Test zero cards, one card, last-card removal, and a page inserted before the current step. Also test minimum/preferred size after adding pages at runtime.
Focus, activation, and accessibility
Visibility changes do not define keyboard focus policy. After switching pages, move focus to an appropriate heading or first actionable control only when that improves the task flow; preserve a user’s deliberate focus when navigation is not a new step. A hidden page should not retain the impression of an active input target. Verify that tab order traverses visible controls rather than hidden content and that keyboard-only users can reach Next, Back, and Cancel consistently.
Give each page a clear heading and provide progress in text when a multi-step process has meaningful order. Do not rely solely on a changing highlight or color. Error messages should be programmatically associated with fields where the app’s UI mechanisms allow it, and focus should land predictably after failed validation. Test screen-reader announcements after page changes and ensure the title does not become stale when the selected card changes.
Avoid using the card index as a user-facing progress number when steps are conditional. A workflow that hides optional pages may have different index counts for different users. The displayed progress model should match the current route and explain whether the count is fixed or approximate.
Archiving and lifecycle boundaries
BCardView and BCardLayout support archiving, but restoring a view hierarchy should not be treated as a replacement for application persistence. Rebuild the model from your versioned document/settings data, then create the corresponding pages and choose the appropriate initial route. If using UI archiving for a specific Haiku workflow, verify how the child views and current selection are restored by the target SDK and handle archive errors.
Each page may own controls, message targets, and asynchronous work. When the parent is detached or the application closes, cancel or invalidate work that would later message a destroyed page. If pages stay allocated but are hidden, they may still have observers or timers; visibility is not the same as paused lifecycle. Start expensive subscriptions only while needed or gate them on explicit page lifecycle messages.
Test transitions, not only screenshots
Test every forward and backward edge, validation failure, cancel path, and conditional branch. Record selected route keys in a small test harness and assert that the visible item matches the route after every transition. Exercise add/remove while the stack has zero, one, and several cards. Verify an invalid index selects no page as documented, and make sure production code prevents that state unless a blank background is intentional.
For layout, vary window size, font scale, locale, and page content. Inspect both preferred/minimum dimensions and actual clipping. For focus, operate with keyboard only, then test assistive navigation after each switch. Simulate a delayed asynchronous response from a prior page and confirm it cannot override a newer route. Reopen a saved workflow and confirm the application model, not a transient view index, determines the correct starting card.
Use BCardView when pages are a small, manageable stack and preserving live page state is useful. Use a different lifecycle or lazy construction strategy when pages are numerous or expensive. In either case, keep routing, validation, persistence, focus, and asynchronous cancellation explicit around the simple card-selection API.
Related:
- Haiku BTabView: Selection, Focus, and Pane Ownership
- How to Build Resizable Haiku Interfaces with BLayout Instead of Fixed Coordinates
Sources: