Skip to content
Haiku OSDeep Dive Published Updated 7 min readViews unavailable

Haiku BView Focus: Keep Keyboard Input Predictable

Treat BView focus as per-window state, distinguish it from activation and selection, and move focus safely when the view tree changes.

In Haiku’s Interface Kit, keyboard focus is a relationship between a window and one view in that window. It is not a process-wide flag, a synonym for the active window, or the same thing as a text selection. When focus handling is left implicit, a custom view may receive mouse input but never keyboard input, a window can restore focus somewhere unexpected, or a view can disappear while it is still the user’s apparent keyboard target.

The practical contract is small: BWindow::CurrentFocus() reports the view that currently owns focus for that window; BView::MakeFocus() requests or relinquishes that state; and BView::IsFocus() lets a view test whether it is the current focus view. A view must be attached to a window for MakeFocus() to have an effect. The current Haiku implementation also keeps focus separate from window activation: deactivating a window does not itself clear the window’s current-focus pointer.

Keep focus, activation, and selection separate

Only one BView is the focus view of a window at a time. Keyboard events such as KeyDown are delivered through the active window’s focus path, while the view’s own selection, caret, or model state remains application data. Moving focus does not automatically select a row, place a text caret where the user expects, or commit an edit. Make each of those transitions explicit in the control that owns the interaction.

Use CurrentFocus() when the window needs to inspect the current target, and IsFocus() when a view needs to update its own presentation. Avoid caching a raw focus-view pointer for long periods: the hierarchy may change, and a removed view is no longer a safe target. Re-read the window’s current state at the point where a transition is made.

MakeFocus(true) asks the attached view to become the focus view. In the current implementation, the previous focus view is told to lose focus before the new one is installed. MakeFocus(false) only clears focus if the receiver is currently the focus view; calling it on some other view does not clear another control’s state. This makes the operation suitable for a specific state transition, but not for indiscriminate cleanup across every view.

Make keyboard navigation eligibility intentional

A custom view that should participate in keyboard focus navigation can be created with the B_NAVIGABLE flag. This is eligibility, not a complete keyboard interaction design. The view still needs useful keyboard behavior, clear focus feedback, and an order that makes sense in the containing interface. Do not assume that adding B_NAVIGABLE will make a mouse-only gesture usable from the keyboard.

For a custom canvas or selectable region whose interaction begins with a pointer click, request focus when the view receives the event:

class SelectableCanvas : public BView {
public:
    SelectableCanvas(BRect frame)
        : BView(frame, "selectable-canvas", B_FOLLOW_ALL,
            B_WILL_DRAW | B_NAVIGABLE)
    {
    }

    void MouseDown(BPoint where) override
    {
        MakeFocus(true);
        SelectAt(where);
    }

    void KeyDown(const char* bytes, int32 numBytes) override
    {
        if (IsFocus())
            HandleFocusedKey(bytes, numBytes);
    }

private:
    void SelectAt(BPoint where);
    void HandleFocusedKey(const char* bytes, int32 numBytes);
};

SelectAt() and HandleFocusedKey() stand for application-specific selection and keyboard policy; the snippet does not define a full editor. A view that accepts focus should also make that state visible with an appropriate focus cue and expose equivalent actions through expected keyboard commands. Do not rely on color alone to show focus, and do not give a decorative view B_NAVIGABLE merely because it can be clicked.

The API reference notes that BView does not automatically become focused just because it received MouseDown. That is useful for many controls: clicking empty space should not necessarily steal focus from a text field. A text editor, canvas, or selectable list should make its intended policy explicit. Conversely, a view that should never receive keyboard input should not call MakeFocus(true) as a generic response to every click.

Do not confuse activation with a focus reset

Window activation determines which window is foreground and can receive keyboard interaction. It does not mean that the application should select a new default view on every activation. Haiku calls WindowActivated() as the window changes active state; in the pinned implementation, deactivation does not clear the current focus view. This allows a window to retain its focused control while the user switches to another window and back.

Overriding WindowActivated() is appropriate when a view has activation-specific presentation or needs to refresh transient display state. It is usually not a reason to call MakeFocus(true) every time the window activates. Doing so can steal focus from a control the user deliberately selected before switching windows. If the product wants a first-focus policy when a window is initially shown, apply it once at the deliberate point where the window is constructed or its interaction mode changes.

A focused view is not necessarily selected content. For example, focus can remain on a list while its selected row changes, or on a text view while its caret and selection move. Keep those model concepts independent so restoring focus does not silently overwrite user selection or pending edits.

Transfer focus before hiding or removing its owner

When a page switch hides a focused control, or a dynamic form removes a focused row, choose an intentional fallback before mutating the hierarchy. The fallback should be visible, attached to the same window, eligible for the next keyboard action, and sensible for the user’s current task. If the view is part of a composite control, focus the actual child that handles keyboard input rather than only the wrapper.

This is especially important for Hide(): in the pinned BView implementation, Hide() changes the view’s visibility level and invalidates layout, but does not call MakeFocus(false). A hidden view can therefore remain the window’s CurrentFocus() unless the application transfers focus. Detaching a view follows a different path: the current implementation relinquishes focus when the focused view loses its owner, but that cleanup does not select a useful replacement for the user.

The ordering matters: first determine whether the current focus lies in the subtree that is about to disappear; then request focus on the fallback; only after that hide, detach, or destroy the old subtree. Do not keep using a pointer obtained from CurrentFocus() after its view has been removed. If there is no valid fallback, it is better to leave the window without a focus view than to retain an invalid assumption about a destroyed object.

Do not treat MakeFocus(false) as a global reset. It only relinquishes focus when called on the current focus view, and clearing focus without setting an appropriate fallback may leave keyboard users with no clear next action. Focus is a navigation and event-routing contract; decide the destination along with the source.

Verify the lifecycle with a small event matrix

Test the same custom view with pointer, keyboard, and activation paths. Confirm it is attached before MakeFocus() is called, that IsFocus() changes as expected, and that the old focus view stops handling focused keys after a transfer. Verify that B_NAVIGABLE controls can be reached through the interface’s keyboard-navigation path and that the user can perceive the current target.

Then test window deactivation and reactivation without changing the view tree. Confirm that the intended focus remains and that activation does not reset a user’s selection or caret. Finally, hide, remove, or replace the focused view and verify that the fallback is selected before the old view becomes unavailable. Repeat with a composite control, a window with no initial focus, and a dialog that closes while an operation is pending.

Useful diagnostics are the window’s CurrentFocus() pointer at transition boundaries and a view’s IsFocus() result when drawing or handling input. Log stable control identifiers rather than pointer values as durable state. Pointer addresses are process-local diagnostics, not identifiers suitable for settings or saved restoration.

Haiku’s focus APIs are straightforward when the view tree is stable. The engineering work is in defining who should own keyboard input through each interaction and lifetime change. Keep focus per-window, activation independent, navigation eligibility intentional, and fallback transfer ahead of view removal; then mouse and keyboard interaction remain aligned without surprising focus grabs.

Related:

Sources:

Comments